Browse Source

Merge pull request #381 from ding113/dev

release v0.3.32
Ding 2 months ago
parent
commit
3aef8dba80
80 changed files with 4659 additions and 776 deletions
  1. 117 0
      AGENTS.md
  2. 115 2
      CLAUDE.md
  3. 12 10
      deploy/Dockerfile
  4. 11 10
      deploy/Dockerfile.dev
  5. 2 0
      drizzle/0038_aberrant_bucky.sql
  6. 1943 0
      drizzle/meta/0038_snapshot.json
  7. 7 0
      drizzle/meta/_journal.json
  8. 0 3
      next.config.ts
  9. 12 5
      src/actions/providers.ts
  10. 63 0
      src/app/[locale]/dashboard/_components/dashboard-sections.tsx
  11. 53 0
      src/app/[locale]/dashboard/_components/dashboard-skeletons.tsx
  12. 18 0
      src/app/[locale]/dashboard/availability/_components/availability-skeleton.tsx
  13. 5 1
      src/app/[locale]/dashboard/availability/page.tsx
  14. 42 20
      src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx
  15. 17 0
      src/app/[locale]/dashboard/loading.tsx
  16. 17 0
      src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx
  17. 34 7
      src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  18. 33 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx
  19. 19 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-skeleton.tsx
  20. 62 11
      src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
  21. 13 37
      src/app/[locale]/dashboard/logs/page.tsx
  22. 26 47
      src/app/[locale]/dashboard/page.tsx
  23. 37 0
      src/app/[locale]/dashboard/providers/_components/providers-skeleton.tsx
  24. 2 12
      src/app/[locale]/dashboard/providers/page.tsx
  25. 14 0
      src/app/[locale]/dashboard/quotas/_components/providers-quota-skeleton.tsx
  26. 14 0
      src/app/[locale]/dashboard/quotas/_components/users-quota-skeleton.tsx
  27. 20 0
      src/app/[locale]/dashboard/quotas/loading.tsx
  28. 21 7
      src/app/[locale]/dashboard/quotas/providers/page.tsx
  29. 16 4
      src/app/[locale]/dashboard/quotas/users/page.tsx
  30. 24 0
      src/app/[locale]/dashboard/rate-limits/_components/rate-limits-skeleton.tsx
  31. 5 3
      src/app/[locale]/dashboard/rate-limits/page.tsx
  32. 25 0
      src/app/[locale]/dashboard/sessions/[sessionId]/messages/loading.tsx
  33. 13 1
      src/app/[locale]/dashboard/sessions/_components/active-sessions-table.tsx
  34. 20 0
      src/app/[locale]/dashboard/sessions/loading.tsx
  35. 25 0
      src/app/[locale]/dashboard/users/_components/users-skeleton.tsx
  36. 1 4
      src/app/[locale]/dashboard/users/page.tsx
  37. 183 104
      src/app/[locale]/dashboard/users/users-page-client.tsx
  38. 20 0
      src/app/[locale]/internal/dashboard/big-screen/loading.tsx
  39. 13 0
      src/app/[locale]/internal/data-gen/loading.tsx
  40. 16 0
      src/app/[locale]/login/loading.tsx
  41. 44 0
      src/app/[locale]/my-usage/_components/loading-states.test.tsx
  42. 32 0
      src/app/[locale]/my-usage/_components/quota-cards.tsx
  43. 43 13
      src/app/[locale]/my-usage/_components/today-usage-card.tsx
  44. 52 19
      src/app/[locale]/my-usage/_components/usage-logs-section.tsx
  45. 25 8
      src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  46. 25 0
      src/app/[locale]/my-usage/loading.tsx
  47. 32 18
      src/app/[locale]/my-usage/page.tsx
  48. 37 0
      src/app/[locale]/settings/client-versions/_components/client-versions-skeleton.tsx
  49. 39 22
      src/app/[locale]/settings/client-versions/page.tsx
  50. 11 0
      src/app/[locale]/settings/config/_components/settings-config-skeleton.tsx
  51. 14 1
      src/app/[locale]/settings/config/page.tsx
  52. 34 0
      src/app/[locale]/settings/error-rules/_components/error-rules-skeleton.tsx
  53. 20 5
      src/app/[locale]/settings/error-rules/page.tsx
  54. 16 0
      src/app/[locale]/settings/loading.tsx
  55. 10 3
      src/app/[locale]/settings/logs/_components/log-level-form.tsx
  56. 285 284
      src/app/[locale]/settings/notifications/page.tsx
  57. 23 0
      src/app/[locale]/settings/prices/_components/prices-skeleton.tsx
  58. 32 21
      src/app/[locale]/settings/prices/page.tsx
  59. 4 0
      src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx
  60. 101 0
      src/app/[locale]/settings/providers/_components/provider-manager-loader.tsx
  61. 70 18
      src/app/[locale]/settings/providers/_components/provider-manager.tsx
  62. 12 0
      src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  63. 12 3
      src/app/[locale]/settings/providers/_components/provider-sort-dropdown.tsx
  64. 4 3
      src/app/[locale]/settings/providers/_components/provider-type-filter.tsx
  65. 4 14
      src/app/[locale]/settings/providers/page.tsx
  66. 26 0
      src/app/[locale]/settings/request-filters/_components/request-filters-skeleton.tsx
  67. 11 3
      src/app/[locale]/settings/request-filters/page.tsx
  68. 33 0
      src/app/[locale]/settings/sensitive-words/_components/sensitive-words-skeleton.tsx
  69. 19 4
      src/app/[locale]/settings/sensitive-words/page.tsx
  70. 23 0
      src/app/[locale]/usage-doc/loading.tsx
  71. 14 11
      src/app/v1/_lib/proxy/forwarder.ts
  72. 27 0
      src/app/v1/_lib/proxy/response-handler.ts
  73. 36 1
      src/components/customs/overview-panel.tsx
  74. 26 0
      src/components/loading/page-skeletons.test.tsx
  75. 163 0
      src/components/loading/page-skeletons.tsx
  76. 2 0
      src/drizzle/schema.ts
  77. 54 0
      src/instrumentation.ts
  78. 22 1
      src/lib/cache/session-cache.ts
  79. 124 36
      src/lib/logger.ts
  80. 8 0
      src/repository/message.ts

+ 117 - 0
AGENTS.md

@@ -1,3 +1,120 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Common Commands
+
+```bash
+bun install                    # Install dependencies
+bun run dev                    # Dev server on port 13500
+bun run build                  # Production build (copies VERSION to standalone)
+bun run lint                   # Biome check
+bun run lint:fix               # Biome auto-fix
+bun run typecheck              # TypeScript check (uses tsgo for speed)
+bun run test                   # Run Vitest tests
+bun run test -- path/to/test   # Run single test file
+bun run test:ui                # Vitest with browser UI
+bun run db:generate            # Generate Drizzle migration (validates afterward)
+bun run db:migrate             # Apply migrations
+bun run db:studio              # Drizzle Studio GUI
+```
+
+## Architecture
+
+### Proxy Request Pipeline (`src/app/v1/_lib/`)
+
+Request flow through `proxy-handler.ts`:
+1. `ProxySession.fromContext()` - Parse incoming request
+2. `detectFormat()` - Identify API format (Claude/OpenAI/Codex)
+3. `GuardPipelineBuilder.run()` - Execute guard chain:
+   - `ProxyAuthenticator` - Validate API key
+   - `SensitiveWordGuard` - Content filtering
+   - `VersionGuard` - Client version check
+   - `ProxySessionGuard` - Session allocation via Redis
+   - `ProxyRateLimitGuard` - Multi-dimensional rate limiting
+   - `ProxyProviderResolver` - Select provider (weight/priority/circuit breaker)
+4. `ProxyForwarder.send()` - Forward with up to 3 retries on failure
+5. `ProxyResponseHandler.dispatch()` - Handle streaming/non-streaming response
+
+### Format Converters (`src/app/v1/_lib/converters/`)
+
+Registry pattern in `registry.ts` maps conversion pairs:
+- Claude <-> OpenAI bidirectional
+- Claude <-> Codex (OpenAI Responses API)
+- OpenAI <-> Codex
+- Gemini CLI adapters
+
+### Core Services (`src/lib/`)
+
+**Session Manager** (`session-manager.ts`):
+- 5-minute Redis context cache with sliding window
+- Decision chain recording for audit trail
+- Session ID extraction from metadata.user_id or messages hash
+
+**Circuit Breaker** (`circuit-breaker.ts`):
+- State machine: CLOSED -> OPEN -> HALF_OPEN -> CLOSED
+- Per-provider isolation with configurable thresholds
+- Redis persistence for multi-instance coordination
+
+**Rate Limiting** (`rate-limit/`):
+- Dimensions: RPM, cost (5h/week/month), concurrent sessions
+- Levels: User, Key, Provider
+- Redis Lua scripts for atomic operations
+- Fail-open when Redis unavailable
+
+### Database (`src/drizzle/`, `src/repository/`)
+
+Drizzle ORM with PostgreSQL. Key tables:
+- `users`, `keys` - Authentication and quotas
+- `providers` - Upstream config (weight, priority, proxy, timeouts)
+- `message_request` - Request logs with decision chain
+- `model_prices` - Token pricing for cost calculation
+- `error_rules`, `request_filters` - Request/response manipulation
+
+Repository pattern in `src/repository/` wraps Drizzle queries.
+
+### Server Actions API (`src/app/api/actions/`)
+
+39 Server Actions auto-exposed as REST endpoints via `[...route]/route.ts`:
+- OpenAPI 3.1.0 spec auto-generated from Zod schemas
+- Swagger UI: `/api/actions/docs`
+- Scalar UI: `/api/actions/scalar`
+
+## Code Style
+
+- Biome: 2-space indent, double quotes, trailing commas, 100 char max line
+- Path alias: `@/*` -> `./src/*`
+- Icons: Use `lucide-react`, no custom SVGs
+- UI components in `src/components/ui/` (excluded from typecheck)
+
+## Testing
+
+Vitest configuration in `vitest.config.ts`:
+- Environment: Node
+- Coverage thresholds: 50% lines/functions, 40% branches
+- Integration tests requiring DB are in `tests/integration/` (excluded by default)
+- Test database must contain 'test' in name for safety
+
+## I18n
+
+5 locales via next-intl: `en`, `ja`, `ru`, `zh-CN`, `zh-TW`
+- Messages: `messages/{locale}/*.json`
+- Routing: `src/i18n/`
+
+## Environment
+
+See `.env.example` for all variables. Critical ones:
+- `ADMIN_TOKEN` - Dashboard login (must change from default)
+- `DSN` - PostgreSQL connection string
+- `REDIS_URL` - Redis for rate limiting and sessions
+- `AUTO_MIGRATE` - Run Drizzle migrations on startup
+
+## Contributing
+
+See `CONTRIBUTING.md` for branch naming, commit format, and PR process.
+All PRs target `dev` branch; `main` is release-only.
+
+
 File: docs/architecture-claude-code-hub-2025-11-29.md
 ```md
 # System Architecture: claude-code-hub

+ 115 - 2
CLAUDE.md

@@ -1,2 +1,115 @@
[email protected]
[email protected]
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Common Commands
+
+```bash
+bun install                    # Install dependencies
+bun run dev                    # Dev server on port 13500
+bun run build                  # Production build (copies VERSION to standalone)
+bun run lint                   # Biome check
+bun run lint:fix               # Biome auto-fix
+bun run typecheck              # TypeScript check (uses tsgo for speed)
+bun run test                   # Run Vitest tests
+bun run test -- path/to/test   # Run single test file
+bun run test:ui                # Vitest with browser UI
+bun run db:generate            # Generate Drizzle migration (validates afterward)
+bun run db:migrate             # Apply migrations
+bun run db:studio              # Drizzle Studio GUI
+```
+
+## Architecture
+
+### Proxy Request Pipeline (`src/app/v1/_lib/`)
+
+Request flow through `proxy-handler.ts`:
+1. `ProxySession.fromContext()` - Parse incoming request
+2. `detectFormat()` - Identify API format (Claude/OpenAI/Codex)
+3. `GuardPipelineBuilder.run()` - Execute guard chain:
+   - `ProxyAuthenticator` - Validate API key
+   - `SensitiveWordGuard` - Content filtering
+   - `VersionGuard` - Client version check
+   - `ProxySessionGuard` - Session allocation via Redis
+   - `ProxyRateLimitGuard` - Multi-dimensional rate limiting
+   - `ProxyProviderResolver` - Select provider (weight/priority/circuit breaker)
+4. `ProxyForwarder.send()` - Forward with up to 3 retries on failure
+5. `ProxyResponseHandler.dispatch()` - Handle streaming/non-streaming response
+
+### Format Converters (`src/app/v1/_lib/converters/`)
+
+Registry pattern in `registry.ts` maps conversion pairs:
+- Claude <-> OpenAI bidirectional
+- Claude <-> Codex (OpenAI Responses API)
+- OpenAI <-> Codex
+- Gemini CLI adapters
+
+### Core Services (`src/lib/`)
+
+**Session Manager** (`session-manager.ts`):
+- 5-minute Redis context cache with sliding window
+- Decision chain recording for audit trail
+- Session ID extraction from metadata.user_id or messages hash
+
+**Circuit Breaker** (`circuit-breaker.ts`):
+- State machine: CLOSED -> OPEN -> HALF_OPEN -> CLOSED
+- Per-provider isolation with configurable thresholds
+- Redis persistence for multi-instance coordination
+
+**Rate Limiting** (`rate-limit/`):
+- Dimensions: RPM, cost (5h/week/month), concurrent sessions
+- Levels: User, Key, Provider
+- Redis Lua scripts for atomic operations
+- Fail-open when Redis unavailable
+
+### Database (`src/drizzle/`, `src/repository/`)
+
+Drizzle ORM with PostgreSQL. Key tables:
+- `users`, `keys` - Authentication and quotas
+- `providers` - Upstream config (weight, priority, proxy, timeouts)
+- `message_request` - Request logs with decision chain
+- `model_prices` - Token pricing for cost calculation
+- `error_rules`, `request_filters` - Request/response manipulation
+
+Repository pattern in `src/repository/` wraps Drizzle queries.
+
+### Server Actions API (`src/app/api/actions/`)
+
+39 Server Actions auto-exposed as REST endpoints via `[...route]/route.ts`:
+- OpenAPI 3.1.0 spec auto-generated from Zod schemas
+- Swagger UI: `/api/actions/docs`
+- Scalar UI: `/api/actions/scalar`
+
+## Code Style
+
+- Biome: 2-space indent, double quotes, trailing commas, 100 char max line
+- Path alias: `@/*` -> `./src/*`
+- Icons: Use `lucide-react`, no custom SVGs
+- UI components in `src/components/ui/` (excluded from typecheck)
+
+## Testing
+
+Vitest configuration in `vitest.config.ts`:
+- Environment: Node
+- Coverage thresholds: 50% lines/functions, 40% branches
+- Integration tests requiring DB are in `tests/integration/` (excluded by default)
+- Test database must contain 'test' in name for safety
+
+## I18n
+
+5 locales via next-intl: `en`, `ja`, `ru`, `zh-CN`, `zh-TW`
+- Messages: `messages/{locale}/*.json`
+- Routing: `src/i18n/`
+
+## Environment
+
+See `.env.example` for all variables. Critical ones:
+- `ADMIN_TOKEN` - Dashboard login (must change from default)
+- `DSN` - PostgreSQL connection string
+- `REDIS_URL` - Redis for rate limiting and sessions
+- `AUTO_MIGRATE` - Run Drizzle migrations on startup
+
+## Contributing
+
+See `CONTRIBUTING.md` for branch naming, commit format, and PR process.
+All PRs target `dev` branch; `main` is release-only.

+ 12 - 10
deploy/Dockerfile

@@ -1,5 +1,6 @@
 # syntax=docker/dockerfile:1
 
+# 构建阶段:使用 Bun(快速包管理和构建)
 FROM --platform=$BUILDPLATFORM oven/bun:debian AS build-base
 WORKDIR /app
 
@@ -26,27 +27,28 @@ ENV CI=true
 
 RUN bun run build
 
-FROM oven/bun:debian AS runner
+# 运行阶段:使用 Node.js(避免 Bun 流式响应内存泄漏 Issue #18488)
+FROM node:trixie-slim AS runner
 ENV NODE_ENV=production
 ENV PORT=3000
 ENV HOST=0.0.0.0
 WORKDIR /app
 
 # 安装 PostgreSQL 客户端工具(用于数据库备份/恢复功能)和 curl(用于健康检查)
-# Debian Trixie 自带 PostgreSQL 17,无需外部 APT 仓库
+# node:trixie-slim 基于 Debian Trixie,需手动安装 postgresql-client
 RUN apt-get update && \
     apt-get install -y curl ca-certificates postgresql-client && \
     rm -rf /var/lib/apt/lists/*
 
-COPY --from=build --chown=bun:bun /app/public ./public
-COPY --from=build --chown=bun:bun /app/drizzle ./drizzle
-COPY --from=build --chown=bun:bun /app/messages ./messages
-COPY --from=build --chown=bun:bun /app/.next/standalone ./
+COPY --from=build --chown=node:node /app/public ./public
+COPY --from=build --chown=node:node /app/drizzle ./drizzle
+COPY --from=build --chown=node:node /app/messages ./messages
+COPY --from=build --chown=node:node /app/.next/standalone ./
 # Server Actions live inside .next/server; copy it or Next.js cannot resolve action IDs.
-COPY --from=build --chown=bun:bun /app/.next/server ./.next/server
-COPY --from=build --chown=bun:bun /app/.next/static ./.next/static
+COPY --from=build --chown=node:node /app/.next/server ./.next/server
+COPY --from=build --chown=node:node /app/.next/static ./.next/static
 
-USER bun
+USER node
 EXPOSE 3000
 
-CMD ["bun", "run", "server.js"]
+CMD ["node", "server.js"]

+ 11 - 10
deploy/Dockerfile.dev

@@ -26,29 +26,30 @@ ENV REDIS_URL="redis://localhost:6379"
 RUN --mount=type=cache,target=/app/.next/cache \
     bun run build
 
-FROM oven/bun:debian AS runner
+# 运行阶段:使用 Node.js(避免 Bun 流式响应内存泄漏 Issue #18488)
+FROM node:trixie-slim AS runner
 ENV NODE_ENV=production
 ENV PORT=3000
 ENV HOST=0.0.0.0
 WORKDIR /app
 
 # 安装 PostgreSQL 客户端工具(用于数据库备份/恢复功能)和 curl(用于健康检查)
-# Debian Trixie 自带 PostgreSQL 17,无需外部 APT 仓库
+# node:trixie-slim 基于 Debian Trixie,需手动安装 postgresql-client
 # 使用 BuildKit 缓存挂载加速 APT 安装
 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
     --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
     apt-get update && \
     apt-get install -y curl ca-certificates postgresql-client
 
-COPY --from=build --chown=bun:bun /app/public ./public
-COPY --from=build --chown=bun:bun /app/drizzle ./drizzle
-COPY --from=build --chown=bun:bun /app/messages ./messages
-COPY --from=build --chown=bun:bun /app/.next/standalone ./
+COPY --from=build --chown=node:node /app/public ./public
+COPY --from=build --chown=node:node /app/drizzle ./drizzle
+COPY --from=build --chown=node:node /app/messages ./messages
+COPY --from=build --chown=node:node /app/.next/standalone ./
 # Server Actions live inside .next/server; copy it or Next.js cannot resolve action IDs.
-COPY --from=build --chown=bun:bun /app/.next/server ./.next/server
-COPY --from=build --chown=bun:bun /app/.next/static ./.next/static
+COPY --from=build --chown=node:node /app/.next/server ./.next/server
+COPY --from=build --chown=node:node /app/.next/static ./.next/static
 
-USER bun
+USER node
 EXPOSE 3000
 
-CMD ["bun", "run", "server.js"]
+CMD ["node", "server.js"]

+ 2 - 0
drizzle/0038_aberrant_bucky.sql

@@ -0,0 +1,2 @@
+ALTER TABLE "message_request" ADD COLUMN "error_stack" text;--> statement-breakpoint
+ALTER TABLE "message_request" ADD COLUMN "error_cause" text;

+ 1943 - 0
drizzle/meta/0038_snapshot.json

@@ -0,0 +1,1943 @@
+{
+  "id": "996478ee-aa77-4651-aacb-248e15110423",
+  "prevId": "d991037a-f11d-420f-b508-439064ed9b06",
+  "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",
+          "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(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "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_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": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "integer",
+          "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
+        },
+        "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_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_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_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_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_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
+        },
+        "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": {}
+        }
+      },
+      "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
+        },
+        "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.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
+        },
+        "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
+        },
+        "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_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
+        },
+        "context_1m_preference": {
+          "name": "context_1m_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_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": {}
+        }
+      },
+      "foreignKeys": {},
+      "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
+        },
+        "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": {}
+        }
+      },
+      "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'"
+        },
+        "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
+        },
+        "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,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "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_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
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -267,6 +267,13 @@
       "when": 1766150400000,
       "tag": "0037_cursor_pagination_index",
       "breakpoints": true
+    },
+    {
+      "idx": 38,
+      "version": "7",
+      "when": 1766151566924,
+      "tag": "0038_aberrant_bucky",
+      "breakpoints": true
     }
   ]
 }

+ 0 - 3
next.config.ts

@@ -21,9 +21,6 @@ const nextConfig: NextConfig = {
     "ioredis",
     "postgres",
     "drizzle-orm",
-    "pino",
-    "pino-pretty",
-    "thread-stream",
   ],
 
   // 强制包含 undici 到 standalone 输出

+ 12 - 5
src/actions/providers.ts

@@ -4,7 +4,12 @@ import { revalidatePath } from "next/cache";
 import { GeminiAuth } from "@/app/v1/_lib/gemini/auth";
 import { isClientAbortError } from "@/app/v1/_lib/proxy/errors";
 import { getSession } from "@/lib/auth";
-import { clearConfigCache, getAllHealthStatusAsync, resetCircuit } from "@/lib/circuit-breaker";
+import {
+  clearConfigCache,
+  clearProviderState,
+  getAllHealthStatusAsync,
+  resetCircuit,
+} from "@/lib/circuit-breaker";
 import { CodexInstructionsCache } from "@/lib/codex-instructions-cache";
 import { PROVIDER_TIMEOUT_DEFAULTS } from "@/lib/constants/provider.constants";
 import { logger } from "@/lib/logger";
@@ -620,14 +625,16 @@ export async function removeProvider(providerId: number): Promise<ActionResult>
 
     await deleteProvider(providerId);
 
-    // 删除 Redis 缓存
+    // 清除内存缓存(无论 Redis 是否成功都要执行)
+    clearConfigCache(providerId);
+    await clearProviderState(providerId);
+
+    // 删除 Redis 缓存(非关键路径,失败时记录警告)
     try {
       await deleteProviderCircuitConfig(providerId);
-      // 清除内存缓存
-      clearConfigCache(providerId);
       logger.debug("removeProvider:cache_cleared", { providerId });
     } catch (error) {
-      logger.warn("removeProvider:cache_clear_failed", {
+      logger.warn("removeProvider:redis_cache_clear_failed", {
         providerId,
         error: error instanceof Error ? error.message : String(error),
       });

+ 63 - 0
src/app/[locale]/dashboard/_components/dashboard-sections.tsx

@@ -0,0 +1,63 @@
+import { ArrowRight } from "lucide-react";
+import { getTranslations } from "next-intl/server";
+import { cache } from "react";
+import { getUserStatistics } from "@/actions/statistics";
+import { OverviewPanel } from "@/components/customs/overview-panel";
+import { Button } from "@/components/ui/button";
+import { Link } from "@/i18n/routing";
+import { getSystemSettings } from "@/repository/system-config";
+import { DEFAULT_TIME_RANGE } from "@/types/statistics";
+import { StatisticsWrapper } from "./statistics";
+import { TodayLeaderboard } from "./today-leaderboard";
+
+const getCachedSystemSettings = cache(getSystemSettings);
+
+export async function DashboardOverviewSection({ isAdmin }: { isAdmin: boolean }) {
+  const systemSettings = await getCachedSystemSettings();
+
+  return <OverviewPanel currencyCode={systemSettings.currencyDisplay} isAdmin={isAdmin} />;
+}
+
+export async function DashboardStatisticsSection() {
+  const [systemSettings, statistics] = await Promise.all([
+    getCachedSystemSettings(),
+    getUserStatistics(DEFAULT_TIME_RANGE),
+  ]);
+
+  return (
+    <StatisticsWrapper
+      initialData={statistics.ok ? statistics.data : undefined}
+      currencyCode={systemSettings.currencyDisplay}
+    />
+  );
+}
+
+export async function DashboardLeaderboardSection({ isAdmin }: { isAdmin: boolean }) {
+  const systemSettings = await getCachedSystemSettings();
+  const canViewLeaderboard = isAdmin || systemSettings.allowGlobalUsageView;
+
+  if (!canViewLeaderboard) {
+    return null;
+  }
+
+  const t = await getTranslations("dashboard");
+
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <h2 className="text-2xl font-bold">{t("leaderboard.todayTitle")}</h2>
+        <Link href="/dashboard/leaderboard">
+          <Button variant="link" size="sm" className="px-0 sm:px-2">
+            {t("leaderboard.viewAll")}
+            <ArrowRight className="ml-1 h-3 w-3" />
+          </Button>
+        </Link>
+      </div>
+      <TodayLeaderboard
+        currencyCode={systemSettings.currencyDisplay}
+        isAdmin={isAdmin}
+        allowGlobalUsageView={systemSettings.allowGlobalUsageView}
+      />
+    </div>
+  );
+}

+ 53 - 0
src/app/[locale]/dashboard/_components/dashboard-skeletons.tsx

@@ -0,0 +1,53 @@
+import { CardGridSkeleton, ListSkeleton, LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function DashboardOverviewSkeleton() {
+  return (
+    <div className="grid grid-cols-1 lg:grid-cols-12 gap-4">
+      <div className="lg:col-span-3 space-y-3">
+        <CardGridSkeleton cards={4} className="grid-cols-2" />
+        <Skeleton className="h-8 w-full" />
+      </div>
+      <div className="lg:col-span-9">
+        <div className="rounded-lg border bg-card">
+          <div className="flex items-center justify-between border-b px-4 py-3">
+            <Skeleton className="h-4 w-28" />
+            <Skeleton className="h-3 w-20" />
+          </div>
+          <div className="p-4">
+            <ListSkeleton rows={6} />
+          </div>
+        </div>
+      </div>
+      <LoadingState />
+    </div>
+  );
+}
+
+export function DashboardStatisticsSkeleton() {
+  return (
+    <div className="rounded-xl border bg-card p-5 space-y-4">
+      <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
+        <Skeleton className="h-5 w-36" />
+        <Skeleton className="h-8 w-40" />
+      </div>
+      <Skeleton className="h-64 w-full" />
+      <LoadingState />
+    </div>
+  );
+}
+
+export function DashboardLeaderboardSkeleton() {
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <Skeleton className="h-6 w-40" />
+        <Skeleton className="h-8 w-24" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <ListSkeleton rows={5} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 18 - 0
src/app/[locale]/dashboard/availability/_components/availability-skeleton.tsx

@@ -0,0 +1,18 @@
+import { ListSkeleton, LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function AvailabilityViewSkeleton() {
+  return (
+    <div className="space-y-4">
+      <div className="flex flex-wrap gap-2">
+        <Skeleton className="h-9 w-32" />
+        <Skeleton className="h-9 w-32" />
+        <Skeleton className="h-9 w-24" />
+      </div>
+      <div className="rounded-lg border bg-card p-4 space-y-3">
+        <ListSkeleton rows={6} />
+      </div>
+      <LoadingState />
+    </div>
+  );
+}

+ 5 - 1
src/app/[locale]/dashboard/availability/page.tsx

@@ -1,9 +1,11 @@
 import { AlertCircle } from "lucide-react";
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { Section } from "@/components/section";
 import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { getSession } from "@/lib/auth";
+import { AvailabilityViewSkeleton } from "./_components/availability-skeleton";
 import { AvailabilityView } from "./_components/availability-view";
 
 export const dynamic = "force-dynamic";
@@ -42,7 +44,9 @@ export default async function AvailabilityPage() {
   return (
     <div className="space-y-6">
       <Section title={t("availability.title")} description={t("availability.description")}>
-        <AvailabilityView />
+        <Suspense fallback={<AvailabilityViewSkeleton />}>
+          <AvailabilityView />
+        </Suspense>
       </Section>
     </div>
   );

+ 42 - 20
src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx

@@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useState } from "react";
 import { Card, CardContent } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
 import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
 import { formatTokenAmount } from "@/lib/utils";
 import type {
@@ -112,25 +113,9 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
     []
   );
 
-  if (loading) {
-    return (
-      <Card>
-        <CardContent className="py-8">
-          <div className="text-center text-muted-foreground">{t("states.loading")}</div>
-        </CardContent>
-      </Card>
-    );
-  }
-
-  if (error) {
-    return (
-      <Card>
-        <CardContent className="py-8">
-          <div className="text-center text-destructive">{error}</div>
-        </CardContent>
-      </Card>
-    );
-  }
+  const skeletonColumns =
+    scope === "user" ? 5 : scope === "provider" ? 7 : scope === "model" ? 6 : 5;
+  const skeletonGridStyle = { gridTemplateColumns: `repeat(${skeletonColumns}, minmax(0, 1fr))` };
 
   // 列定义(根据 scope 动态切换)
   const userColumns: ColumnDef<UserEntry>[] = [
@@ -280,7 +265,44 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
 
       {/* 数据表格 */}
       <div>
-        <LeaderboardTable data={data} period={period} columns={columns} getRowKey={rowKey} />
+        {loading ? (
+          <Card>
+            <CardContent className="py-6 space-y-4">
+              <div className="space-y-3">
+                <div className="grid gap-4" style={skeletonGridStyle}>
+                  {Array.from({ length: skeletonColumns }).map((_, index) => (
+                    <Skeleton key={`leaderboard-head-${index}`} className="h-4 w-full" />
+                  ))}
+                </div>
+                <div className="space-y-2">
+                  {Array.from({ length: 6 }).map((_, rowIndex) => (
+                    <div
+                      key={`leaderboard-row-${rowIndex}`}
+                      className="grid gap-4"
+                      style={skeletonGridStyle}
+                    >
+                      {Array.from({ length: skeletonColumns }).map((_, colIndex) => (
+                        <Skeleton
+                          key={`leaderboard-cell-${rowIndex}-${colIndex}`}
+                          className="h-4 w-full"
+                        />
+                      ))}
+                    </div>
+                  ))}
+                </div>
+              </div>
+              <div className="text-center text-xs text-muted-foreground">{t("states.loading")}</div>
+            </CardContent>
+          </Card>
+        ) : error ? (
+          <Card>
+            <CardContent className="py-8">
+              <div className="text-center text-destructive">{error}</div>
+            </CardContent>
+          </Card>
+        ) : (
+          <LeaderboardTable data={data} period={period} columns={columns} getRowKey={rowKey} />
+        )}
       </div>
     </div>
   );

+ 17 - 0
src/app/[locale]/dashboard/loading.tsx

@@ -0,0 +1,17 @@
+import {
+  LoadingState,
+  PageHeaderSkeleton,
+  SectionSkeleton,
+  TableSkeleton,
+} from "@/components/loading/page-skeletons";
+
+export default function DashboardLoading() {
+  return (
+    <div className="space-y-6">
+      <PageHeaderSkeleton titleWidth="w-52" descriptionWidth="w-80" />
+      <SectionSkeleton body={<TableSkeleton rows={5} columns={4} />} />
+      <SectionSkeleton rows={4} />
+      <LoadingState className="text-center" />
+    </div>
+  );
+}

+ 17 - 0
src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx

@@ -0,0 +1,17 @@
+import { ListSkeleton, LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ActiveSessionsSkeleton() {
+  return (
+    <div className="rounded-lg border bg-card">
+      <div className="flex items-center justify-between border-b px-4 py-3">
+        <Skeleton className="h-4 w-28" />
+        <Skeleton className="h-3 w-20" />
+      </div>
+      <div className="p-4 space-y-3">
+        <ListSkeleton rows={5} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 34 - 7
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx

@@ -36,6 +36,9 @@ interface UsageLogsFiltersProps {
   users: UserDisplay[];
   providers: ProviderDisplay[];
   initialKeys: Key[];
+  isUsersLoading?: boolean;
+  isProvidersLoading?: boolean;
+  isKeysLoading?: boolean;
   filters: {
     userId?: number;
     keyId?: number;
@@ -59,6 +62,9 @@ export function UsageLogsFilters({
   users,
   providers,
   initialKeys,
+  isUsersLoading = false,
+  isProvidersLoading = false,
+  isKeysLoading = false,
   filters,
   onChange,
   onReset,
@@ -94,6 +100,12 @@ export function UsageLogsFilters({
   const [localFilters, setLocalFilters] = useState(filters);
   const [isExporting, setIsExporting] = useState(false);
 
+  useEffect(() => {
+    if (initialKeys.length > 0) {
+      setKeys(initialKeys);
+    }
+  }, [initialKeys]);
+
   // 管理员用户首次加载时,如果 URL 中有 userId 参数,需要加载该用户的 keys
   // biome-ignore lint/correctness/useExhaustiveDependencies: 故意仅在组件挂载时执行一次
   useEffect(() => {
@@ -251,9 +263,17 @@ export function UsageLogsFilters({
         {isAdmin && (
           <div className="space-y-2 lg:col-span-4">
             <Label>{t("logs.filters.user")}</Label>
-            <Select value={localFilters.userId?.toString() || ""} onValueChange={handleUserChange}>
+            <Select
+              value={localFilters.userId?.toString() || ""}
+              onValueChange={handleUserChange}
+              disabled={isUsersLoading}
+            >
               <SelectTrigger>
-                <SelectValue placeholder={t("logs.filters.allUsers")} />
+                <SelectValue
+                  placeholder={
+                    isUsersLoading ? t("logs.stats.loading") : t("logs.filters.allUsers")
+                  }
+                />
               </SelectTrigger>
               <SelectContent>
                 {users.map((user) => (
@@ -277,14 +297,16 @@ export function UsageLogsFilters({
                 keyId: value ? parseInt(value, 10) : undefined,
               })
             }
-            disabled={isAdmin && !localFilters.userId && keys.length === 0}
+            disabled={isKeysLoading || (isAdmin && !localFilters.userId && keys.length === 0)}
           >
             <SelectTrigger>
               <SelectValue
                 placeholder={
-                  isAdmin && !localFilters.userId && keys.length === 0
-                    ? t("logs.filters.selectUserFirst")
-                    : t("logs.filters.allKeys")
+                  isKeysLoading
+                    ? t("logs.stats.loading")
+                    : isAdmin && !localFilters.userId && keys.length === 0
+                      ? t("logs.filters.selectUserFirst")
+                      : t("logs.filters.allKeys")
                 }
               />
             </SelectTrigger>
@@ -310,9 +332,14 @@ export function UsageLogsFilters({
                   providerId: value ? parseInt(value, 10) : undefined,
                 })
               }
+              disabled={isProvidersLoading}
             >
               <SelectTrigger>
-                <SelectValue placeholder={t("logs.filters.allProviders")} />
+                <SelectValue
+                  placeholder={
+                    isProvidersLoading ? t("logs.stats.loading") : t("logs.filters.allProviders")
+                  }
+                />
               </SelectTrigger>
               <SelectContent>
                 {providers.map((provider) => (

+ 33 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx

@@ -0,0 +1,33 @@
+import { cache } from "react";
+import { ActiveSessionsPanel } from "@/components/customs/active-sessions-panel";
+import { getSystemSettings } from "@/repository/system-config";
+import { UsageLogsViewVirtualized } from "./usage-logs-view-virtualized";
+
+const getCachedSystemSettings = cache(getSystemSettings);
+
+interface UsageLogsDataSectionProps {
+  isAdmin: boolean;
+  userId: number;
+  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}
+
+export async function UsageLogsActiveSessionsSection() {
+  const systemSettings = await getCachedSystemSettings();
+  return <ActiveSessionsPanel currencyCode={systemSettings.currencyDisplay} />;
+}
+
+export async function UsageLogsDataSection({
+  isAdmin,
+  userId,
+  searchParams,
+}: UsageLogsDataSectionProps) {
+  const resolvedSearchParams = await searchParams;
+
+  return (
+    <UsageLogsViewVirtualized
+      isAdmin={isAdmin}
+      userId={userId}
+      searchParams={resolvedSearchParams}
+    />
+  );
+}

+ 19 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-skeleton.tsx

@@ -0,0 +1,19 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function UsageLogsSkeleton() {
+  return (
+    <div className="space-y-4">
+      <div className="grid gap-3 md:grid-cols-3">
+        <Skeleton className="h-10 w-full" />
+        <Skeleton className="h-10 w-full" />
+        <Skeleton className="h-10 w-full" />
+      </div>
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <Skeleton className="h-6 w-32" />
+        <TableSkeleton rows={8} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 62 - 11
src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx

@@ -1,16 +1,19 @@
 "use client";
 
-import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query";
+import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from "@tanstack/react-query";
 import { Pause, Play, RefreshCw } from "lucide-react";
 import { useRouter, useSearchParams } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { getKeys } from "@/actions/keys";
+import { getProviders } from "@/actions/providers";
+import { getUsers } from "@/actions/users";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import type { Key } from "@/types/key";
 import type { ProviderDisplay } from "@/types/provider";
-import type { BillingModelSource } from "@/types/system-config";
+import type { BillingModelSource, SystemSettings } from "@/types/system-config";
 import type { UserDisplay } from "@/types/user";
 import { UsageLogsFilters } from "./usage-logs-filters";
 import { UsageLogsStatsPanel } from "./usage-logs-stats-panel";
@@ -28,16 +31,26 @@ const queryClient = new QueryClient({
 
 interface UsageLogsViewVirtualizedProps {
   isAdmin: boolean;
-  users: UserDisplay[];
-  providers: ProviderDisplay[];
-  initialKeys: Key[];
+  userId: number;
+  users?: UserDisplay[];
+  providers?: ProviderDisplay[];
+  initialKeys?: Key[];
   searchParams: { [key: string]: string | string[] | undefined };
   currencyCode?: CurrencyCode;
   billingModelSource?: BillingModelSource;
 }
 
+async function fetchSystemSettings(): Promise<SystemSettings> {
+  const response = await fetch("/api/system-settings");
+  if (!response.ok) {
+    throw new Error("FETCH_SETTINGS_FAILED");
+  }
+  return response.json() as Promise<SystemSettings>;
+}
+
 function UsageLogsViewContent({
   isAdmin,
+  userId,
   users,
   providers,
   initialKeys,
@@ -54,6 +67,41 @@ function UsageLogsViewContent({
   const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
   const paramsKey = _params.toString();
 
+  const shouldFetchSettings = !currencyCode || !billingModelSource;
+  const { data: systemSettings } = useQuery<SystemSettings>({
+    queryKey: ["system-settings"],
+    queryFn: fetchSystemSettings,
+    enabled: shouldFetchSettings,
+  });
+
+  const resolvedCurrencyCode = currencyCode ?? systemSettings?.currencyDisplay ?? "USD";
+  const resolvedBillingModelSource =
+    billingModelSource ?? systemSettings?.billingModelSource ?? "original";
+
+  const { data: usersData = [], isLoading: isUsersLoading } = useQuery<UserDisplay[]>({
+    queryKey: ["usage-log-users"],
+    queryFn: getUsers,
+    enabled: isAdmin && users === undefined,
+    initialData: users ?? [],
+  });
+
+  const { data: providersData = [], isLoading: isProvidersLoading } = useQuery<ProviderDisplay[]>({
+    queryKey: ["usage-log-providers"],
+    queryFn: getProviders,
+    enabled: isAdmin && providers === undefined,
+    initialData: providers ?? [],
+  });
+
+  const { data: keysResult, isLoading: isKeysLoading } = useQuery({
+    queryKey: ["usage-log-keys", userId],
+    queryFn: () => getKeys(userId),
+    enabled: !isAdmin && initialKeys === undefined,
+  });
+
+  const resolvedUsers = users ?? usersData;
+  const resolvedProviders = providers ?? providersData;
+  const resolvedKeys = initialKeys ?? (keysResult?.ok && keysResult.data ? keysResult.data : []);
+
   // Parse filters from URL with stable reference
   const filters = useMemo<VirtualizedLogsTableFilters & { page?: number }>(
     () => ({
@@ -152,7 +200,7 @@ function UsageLogsViewContent({
           endpoint: filters.endpoint,
           minRetryCount: filters.minRetryCount,
         }}
-        currencyCode={currencyCode}
+        currencyCode={resolvedCurrencyCode}
       />
 
       {/* Filters */}
@@ -163,12 +211,15 @@ function UsageLogsViewContent({
         <CardContent>
           <UsageLogsFilters
             isAdmin={isAdmin}
-            users={users}
-            providers={providers}
-            initialKeys={initialKeys}
+            users={resolvedUsers}
+            providers={resolvedProviders}
+            initialKeys={resolvedKeys}
             filters={filters}
             onChange={handleFilterChange}
             onReset={() => router.push("/dashboard/logs")}
+            isUsersLoading={isUsersLoading}
+            isProvidersLoading={isProvidersLoading}
+            isKeysLoading={isKeysLoading}
           />
         </CardContent>
       </Card>
@@ -210,8 +261,8 @@ function UsageLogsViewContent({
         <CardContent className="px-0">
           <VirtualizedLogsTable
             filters={filters}
-            currencyCode={currencyCode}
-            billingModelSource={billingModelSource}
+            currencyCode={resolvedCurrencyCode}
+            billingModelSource={resolvedBillingModelSource}
             autoRefreshEnabled={isAutoRefresh}
             autoRefreshIntervalMs={5000}
           />

+ 13 - 37
src/app/[locale]/dashboard/logs/page.tsx

@@ -1,14 +1,14 @@
 import { getTranslations } from "next-intl/server";
 import { Suspense } from "react";
-import { getKeys } from "@/actions/keys";
-import { getProviders } from "@/actions/providers";
-import { getUsers } from "@/actions/users";
-import { ActiveSessionsPanel } from "@/components/customs/active-sessions-panel";
 import { Section } from "@/components/section";
 import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
-import { UsageLogsViewVirtualized } from "./_components/usage-logs-view-virtualized";
+import { ActiveSessionsSkeleton } from "./_components/active-sessions-skeleton";
+import {
+  UsageLogsActiveSessionsSection,
+  UsageLogsDataSection,
+} from "./_components/usage-logs-sections";
+import { UsageLogsSkeleton } from "./_components/usage-logs-skeleton";
 
 export const dynamic = "force-dynamic";
 
@@ -31,42 +31,18 @@ export default async function UsageLogsPage({
 
   const t = await getTranslations("dashboard");
 
-  // 管理员:获取用户和供应商列表
-  // 非管理员:获取当前用户的 Keys 列表
-  const [users, providers, initialKeys, resolvedSearchParams, systemSettings] = isAdmin
-    ? await Promise.all([
-        getUsers(),
-        getProviders(),
-        Promise.resolve({ ok: true, data: [] }),
-        searchParams,
-        getSystemSettings(),
-      ])
-    : await Promise.all([
-        Promise.resolve([]),
-        Promise.resolve([]),
-        getKeys(session.user.id),
-        searchParams,
-        getSystemSettings(),
-      ]);
-
   return (
     <div className="space-y-6">
-      <ActiveSessionsPanel currencyCode={systemSettings.currencyDisplay} />
+      <Suspense fallback={<ActiveSessionsSkeleton />}>
+        <UsageLogsActiveSessionsSection />
+      </Suspense>
 
       <Section title={t("title.usageLogs")} description={t("title.usageLogsDescription")}>
-        <Suspense
-          fallback={
-            <div className="text-center py-8 text-muted-foreground">{t("logs.stats.loading")}</div>
-          }
-        >
-          <UsageLogsViewVirtualized
+        <Suspense fallback={<UsageLogsSkeleton />}>
+          <UsageLogsDataSection
             isAdmin={isAdmin}
-            users={users}
-            providers={providers}
-            initialKeys={initialKeys.ok ? initialKeys.data : []}
-            searchParams={resolvedSearchParams}
-            currencyCode={systemSettings.currencyDisplay}
-            billingModelSource={systemSettings.billingModelSource}
+            userId={session.user.id}
+            searchParams={searchParams}
           />
         </Suspense>
       </Section>

+ 26 - 47
src/app/[locale]/dashboard/page.tsx

@@ -1,15 +1,17 @@
-import { ArrowRight } from "lucide-react";
-import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { hasPriceTable } from "@/actions/model-prices";
-import { getUserStatistics } from "@/actions/statistics";
-import { OverviewPanel } from "@/components/customs/overview-panel";
-import { Button } from "@/components/ui/button";
-import { Link, redirect } from "@/i18n/routing";
+import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
-import { DEFAULT_TIME_RANGE } from "@/types/statistics";
-import { StatisticsWrapper } from "./_components/statistics";
-import { TodayLeaderboard } from "./_components/today-leaderboard";
+import {
+  DashboardLeaderboardSection,
+  DashboardOverviewSection,
+  DashboardStatisticsSection,
+} from "./_components/dashboard-sections";
+import {
+  DashboardLeaderboardSkeleton,
+  DashboardOverviewSkeleton,
+  DashboardStatisticsSkeleton,
+} from "./_components/dashboard-skeletons";
 
 export const dynamic = "force-dynamic";
 
@@ -17,53 +19,30 @@ export default async function DashboardPage({ params }: { params: Promise<{ loca
   // Await params to ensure locale is available in the async context
   const { locale } = await params;
 
-  const t = await getTranslations("dashboard");
-
   // 检查价格表是否存在,如果不存在则跳转到价格上传页面
   const hasPrices = await hasPriceTable();
   if (!hasPrices) {
     return redirect({ href: "/settings/prices?required=true", locale });
   }
 
-  const [session, statistics, systemSettings] = await Promise.all([
-    getSession(),
-    getUserStatistics(DEFAULT_TIME_RANGE),
-    getSystemSettings(),
-  ]);
-
-  // 检查是否是 admin 用户
+  const session = await getSession();
   const isAdmin = session?.user?.role === "admin";
-  const canViewLeaderboard = isAdmin || systemSettings.allowGlobalUsageView;
 
   return (
     <div className="space-y-6">
-      <OverviewPanel currencyCode={systemSettings.currencyDisplay} isAdmin={isAdmin} />
-
-      <div>
-        <StatisticsWrapper
-          initialData={statistics.ok ? statistics.data : undefined}
-          currencyCode={systemSettings.currencyDisplay}
-        />
-      </div>
-
-      {canViewLeaderboard && (
-        <div className="space-y-4">
-          <div className="flex items-center justify-between">
-            <h2 className="text-2xl font-bold">{t("leaderboard.todayTitle")}</h2>
-            <Link href="/dashboard/leaderboard">
-              <Button variant="link" size="sm" className="px-0 sm:px-2">
-                {t("leaderboard.viewAll")}
-                <ArrowRight className="ml-1 h-3 w-3" />
-              </Button>
-            </Link>
-          </div>
-          <TodayLeaderboard
-            currencyCode={systemSettings.currencyDisplay}
-            isAdmin={isAdmin}
-            allowGlobalUsageView={systemSettings.allowGlobalUsageView}
-          />
-        </div>
-      )}
+      {isAdmin ? (
+        <Suspense fallback={<DashboardOverviewSkeleton />}>
+          <DashboardOverviewSection isAdmin={isAdmin} />
+        </Suspense>
+      ) : null}
+
+      <Suspense fallback={<DashboardStatisticsSkeleton />}>
+        <DashboardStatisticsSection />
+      </Suspense>
+
+      <Suspense fallback={<DashboardLeaderboardSkeleton />}>
+        <DashboardLeaderboardSection isAdmin={isAdmin} />
+      </Suspense>
     </div>
   );
 }

+ 37 - 0
src/app/[locale]/dashboard/providers/_components/providers-skeleton.tsx

@@ -0,0 +1,37 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ProvidersSectionSkeleton() {
+  return (
+    <div className="space-y-4">
+      <TableSkeleton rows={6} columns={6} />
+      <LoadingState />
+    </div>
+  );
+}
+
+export function ProvidersPageSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="space-y-2">
+        <Skeleton className="h-7 w-48" />
+        <Skeleton className="h-4 w-72" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-4">
+        <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+          <div className="space-y-2">
+            <Skeleton className="h-5 w-32" />
+            <Skeleton className="h-4 w-64" />
+          </div>
+          <div className="flex flex-wrap gap-2">
+            <Skeleton className="h-9 w-28" />
+            <Skeleton className="h-9 w-28" />
+            <Skeleton className="h-9 w-28" />
+          </div>
+        </div>
+        <TableSkeleton rows={6} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 2 - 12
src/app/[locale]/dashboard/providers/page.tsx

@@ -1,15 +1,13 @@
 import { BarChart3 } from "lucide-react";
 import { getTranslations } from "next-intl/server";
-import { getProviders, getProvidersHealthStatus } from "@/actions/providers";
 import { AddProviderDialog } from "@/app/[locale]/settings/providers/_components/add-provider-dialog";
-import { ProviderManager } from "@/app/[locale]/settings/providers/_components/provider-manager";
+import { ProviderManagerLoader } from "@/app/[locale]/settings/providers/_components/provider-manager-loader";
 import { SchedulingRulesDialog } from "@/app/[locale]/settings/providers/_components/scheduling-rules-dialog";
 import { Section } from "@/components/section";
 import { Button } from "@/components/ui/button";
 import { Link, redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
 import { getEnvConfig } from "@/lib/config/env.schema";
-import { getSystemSettings } from "@/repository/system-config";
 
 export const dynamic = "force-dynamic";
 
@@ -31,11 +29,6 @@ export default async function DashboardProvidersPage({
   const currentUser = session!.user;
 
   const t = await getTranslations("settings");
-  const [providers, healthStatus, systemSettings] = await Promise.all([
-    getProviders(),
-    getProvidersHealthStatus(),
-    getSystemSettings(),
-  ]);
 
   // 读取多供应商类型支持配置
   const enableMultiProviderTypes = getEnvConfig().ENABLE_MULTI_PROVIDER_TYPES;
@@ -63,11 +56,8 @@ export default async function DashboardProvidersPage({
           </>
         }
       >
-        <ProviderManager
-          providers={providers}
+        <ProviderManagerLoader
           currentUser={currentUser}
-          healthStatus={healthStatus}
-          currencyCode={systemSettings.currencyDisplay}
           enableMultiProviderTypes={enableMultiProviderTypes}
         />
       </Section>

+ 14 - 0
src/app/[locale]/dashboard/quotas/_components/providers-quota-skeleton.tsx

@@ -0,0 +1,14 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ProvidersQuotaSkeleton() {
+  return (
+    <div className="space-y-3">
+      <Skeleton className="h-4 w-32" />
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 14 - 0
src/app/[locale]/dashboard/quotas/_components/users-quota-skeleton.tsx

@@ -0,0 +1,14 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function UsersQuotaSkeleton() {
+  return (
+    <div className="space-y-3">
+      <Skeleton className="h-4 w-32" />
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 20 - 0
src/app/[locale]/dashboard/quotas/loading.tsx

@@ -0,0 +1,20 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function QuotasLoading() {
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <div className="space-y-2">
+          <Skeleton className="h-5 w-36" />
+          <Skeleton className="h-4 w-48" />
+        </div>
+        <Skeleton className="h-9 w-28" />
+      </div>
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 21 - 7
src/app/[locale]/dashboard/quotas/providers/page.tsx

@@ -1,6 +1,8 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { getProviderLimitUsageBatch, getProviders } from "@/actions/providers";
 import { getSystemSettings } from "@/repository/system-config";
+import { ProvidersQuotaSkeleton } from "../_components/providers-quota-skeleton";
 import { ProvidersQuotaManager } from "./_components/providers-quota-manager";
 
 // 强制动态渲染 (此页面需要实时数据和认证)
@@ -37,10 +39,6 @@ async function getProvidersWithQuotas() {
 }
 
 export default async function ProvidersQuotaPage() {
-  const [providers, systemSettings] = await Promise.all([
-    getProvidersWithQuotas(),
-    getSystemSettings(),
-  ]);
   const t = await getTranslations("quota.providers");
 
   return (
@@ -48,12 +46,28 @@ export default async function ProvidersQuotaPage() {
       <div className="flex items-center justify-between">
         <div>
           <h3 className="text-lg font-medium">{t("title")}</h3>
-          <p className="text-sm text-muted-foreground">
-            {t("totalCount", { count: providers.length })}
-          </p>
         </div>
       </div>
 
+      <Suspense fallback={<ProvidersQuotaSkeleton />}>
+        <ProvidersQuotaContent />
+      </Suspense>
+    </div>
+  );
+}
+
+async function ProvidersQuotaContent() {
+  const [providers, systemSettings] = await Promise.all([
+    getProvidersWithQuotas(),
+    getSystemSettings(),
+  ]);
+  const t = await getTranslations("quota.providers");
+
+  return (
+    <div className="space-y-3">
+      <p className="text-sm text-muted-foreground">
+        {t("totalCount", { count: providers.length })}
+      </p>
       <ProvidersQuotaManager providers={providers} currencyCode={systemSettings.currencyDisplay} />
     </div>
   );

+ 16 - 4
src/app/[locale]/dashboard/quotas/users/page.tsx

@@ -1,11 +1,13 @@
 import { Info } from "lucide-react";
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { getUserLimitUsage, getUsers } from "@/actions/users";
 import { QuotaToolbar } from "@/components/quota/quota-toolbar";
 import { Alert, AlertDescription } from "@/components/ui/alert";
 import { Link } from "@/i18n/routing";
 import { sumKeyTotalCostById, sumUserTotalCost } from "@/repository/statistics";
 import { getSystemSettings } from "@/repository/system-config";
+import { UsersQuotaSkeleton } from "../_components/users-quota-skeleton";
 import type { UserKeyWithUsage, UserQuotaWithUsage } from "./_components/types";
 import { UsersQuotaClient } from "./_components/users-quota-client";
 
@@ -73,7 +75,6 @@ async function getUsersWithQuotas(): Promise<UserQuotaWithUsage[]> {
 }
 
 export default async function UsersQuotaPage() {
-  const [users, systemSettings] = await Promise.all([getUsersWithQuotas(), getSystemSettings()]);
   const t = await getTranslations("quota.users");
 
   return (
@@ -81,9 +82,6 @@ export default async function UsersQuotaPage() {
       <div className="flex items-center justify-between">
         <div>
           <h3 className="text-lg font-medium">{t("title")}</h3>
-          <p className="text-sm text-muted-foreground">
-            {t("totalCount", { count: users.length })}
-          </p>
         </div>
       </div>
 
@@ -109,6 +107,20 @@ export default async function UsersQuotaPage() {
         ]}
       />
 
+      <Suspense fallback={<UsersQuotaSkeleton />}>
+        <UsersQuotaContent />
+      </Suspense>
+    </div>
+  );
+}
+
+async function UsersQuotaContent() {
+  const [users, systemSettings] = await Promise.all([getUsersWithQuotas(), getSystemSettings()]);
+  const t = await getTranslations("quota.users");
+
+  return (
+    <div className="space-y-3">
+      <p className="text-sm text-muted-foreground">{t("totalCount", { count: users.length })}</p>
       <UsersQuotaClient users={users} currencyCode={systemSettings.currencyDisplay} />
     </div>
   );

+ 24 - 0
src/app/[locale]/dashboard/rate-limits/_components/rate-limits-skeleton.tsx

@@ -0,0 +1,24 @@
+import { LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function RateLimitsContentSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="flex flex-wrap gap-2">
+        <Skeleton className="h-9 w-40" />
+        <Skeleton className="h-9 w-40" />
+        <Skeleton className="h-9 w-24" />
+      </div>
+      <div className="grid gap-4 md:grid-cols-3">
+        {Array.from({ length: 3 }).map((_, index) => (
+          <div key={`rate-limit-card-${index}`} className="rounded-lg border bg-card p-4">
+            <Skeleton className="h-4 w-24" />
+            <Skeleton className="h-8 w-20 mt-2" />
+          </div>
+        ))}
+      </div>
+      <Skeleton className="h-64 w-full rounded-xl" />
+      <LoadingState />
+    </div>
+  );
+}

+ 5 - 3
src/app/[locale]/dashboard/rate-limits/page.tsx

@@ -1,9 +1,10 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { Section } from "@/components/section";
 import { redirect } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
-import { getSystemSettings } from "@/repository/system-config";
 import { RateLimitDashboard } from "./_components/rate-limit-dashboard";
+import { RateLimitsContentSkeleton } from "./_components/rate-limits-skeleton";
 
 export const dynamic = "force-dynamic";
 
@@ -17,12 +18,13 @@ export default async function RateLimitsPage({ params }: { params: Promise<{ loc
   }
 
   const t = await getTranslations("dashboard.rateLimits");
-  const systemSettings = await getSystemSettings();
 
   return (
     <div className="space-y-6">
       <Section title={t("title")} description={t("description")}>
-        <RateLimitDashboard currencyCode={systemSettings.currencyDisplay} />
+        <Suspense fallback={<RateLimitsContentSkeleton />}>
+          <RateLimitDashboard />
+        </Suspense>
       </Section>
     </div>
   );

+ 25 - 0
src/app/[locale]/dashboard/sessions/[sessionId]/messages/loading.tsx

@@ -0,0 +1,25 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function SessionMessagesLoading() {
+  return (
+    <div className="flex h-full">
+      <aside className="w-72 border-r bg-card p-4 space-y-3">
+        <Skeleton className="h-5 w-32" />
+        <TableSkeleton rows={6} columns={1} />
+      </aside>
+      <div className="flex-1 p-6 space-y-6">
+        <div className="flex items-center justify-between">
+          <Skeleton className="h-9 w-28" />
+          <div className="flex gap-2">
+            <Skeleton className="h-9 w-24" />
+            <Skeleton className="h-9 w-24" />
+          </div>
+        </div>
+        <Skeleton className="h-48 w-full" />
+        <Skeleton className="h-64 w-full" />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 13 - 1
src/app/[locale]/dashboard/sessions/_components/active-sessions-table.tsx

@@ -18,6 +18,7 @@ import {
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Checkbox } from "@/components/ui/checkbox";
+import { Skeleton } from "@/components/ui/skeleton";
 import {
   Table,
   TableBody,
@@ -190,6 +191,7 @@ export function ActiveSessionsTable({
   };
 
   const totalColumns = showSelection ? 12 : 11;
+  const showLoadingRows = isLoading && sessions.length === 0;
   const allSelected =
     showSelection &&
     selectedSessionIds.length > 0 &&
@@ -280,7 +282,17 @@ export function ActiveSessionsTable({
             </TableRow>
           </TableHeader>
           <TableBody>
-            {sortedSessions.length === 0 ? (
+            {showLoadingRows ? (
+              Array.from({ length: 6 }).map((_, rowIndex) => (
+                <TableRow key={`loading-row-${rowIndex}`}>
+                  {Array.from({ length: totalColumns }).map((_, colIndex) => (
+                    <TableCell key={`loading-cell-${rowIndex}-${colIndex}`}>
+                      <Skeleton className="h-4 w-full" />
+                    </TableCell>
+                  ))}
+                </TableRow>
+              ))
+            ) : sortedSessions.length === 0 ? (
               <TableRow>
                 <TableCell colSpan={totalColumns} className="text-center text-muted-foreground">
                   {t("table.noActiveSessions")}

+ 20 - 0
src/app/[locale]/dashboard/sessions/loading.tsx

@@ -0,0 +1,20 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function SessionsLoading() {
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center gap-4">
+        <Skeleton className="h-9 w-24" />
+        <div className="space-y-2">
+          <Skeleton className="h-6 w-40" />
+          <Skeleton className="h-4 w-64" />
+        </div>
+      </div>
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={6} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 25 - 0
src/app/[locale]/dashboard/users/_components/users-skeleton.tsx

@@ -0,0 +1,25 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function UsersPageSkeleton() {
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <div className="space-y-2">
+          <Skeleton className="h-6 w-40" />
+          <Skeleton className="h-4 w-56" />
+        </div>
+        <Skeleton className="h-9 w-32" />
+      </div>
+      <div className="flex flex-wrap gap-2">
+        <Skeleton className="h-9 w-48" />
+        <Skeleton className="h-9 w-32" />
+        <Skeleton className="h-9 w-32" />
+      </div>
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={5} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 1 - 4
src/app/[locale]/dashboard/users/page.tsx

@@ -1,5 +1,4 @@
 import { redirect } from "next/navigation";
-import { getUsers } from "@/actions/users";
 import { getSession } from "@/lib/auth";
 import { UsersPageClient } from "./users-page-client";
 
@@ -11,7 +10,5 @@ export default async function UsersPage() {
     redirect("/login");
   }
 
-  const users = await getUsers();
-
-  return <UsersPageClient users={users} currentUser={session.user} />;
+  return <UsersPageClient currentUser={session.user} />;
 }

+ 183 - 104
src/app/[locale]/dashboard/users/users-page-client.tsx

@@ -1,8 +1,10 @@
 "use client";
 
-import { Plus, Search } from "lucide-react";
+import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
+import { Loader2, Plus, Search } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useMemo, useState } from "react";
+import { getUsers } from "@/actions/users";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
@@ -13,12 +15,21 @@ import {
   SelectTrigger,
   SelectValue,
 } from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
 import type { User, UserDisplay } from "@/types/user";
 import { UnifiedEditDialog } from "../_components/user/unified-edit-dialog";
 import { UserManagementTable } from "../_components/user/user-management-table";
 import { UserOnboardingTour } from "../_components/user/user-onboarding-tour";
 
 const ONBOARDING_KEY = "cch-users-onboarding-seen";
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      refetchOnWindowFocus: false,
+      staleTime: 30000,
+    },
+  },
+});
 
 /**
  * Split comma-separated tags into an array of trimmed, non-empty strings.
@@ -32,16 +43,38 @@ function splitTags(value?: string | null): string[] {
 }
 
 interface UsersPageClientProps {
-  users: UserDisplay[];
+  initialUsers?: UserDisplay[];
   currentUser: User;
 }
 
-export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
+export function UsersPageClient(props: UsersPageClientProps) {
+  return (
+    <QueryClientProvider client={queryClient}>
+      <UsersPageContent {...props} />
+    </QueryClientProvider>
+  );
+}
+
+function UsersPageContent({ initialUsers, currentUser }: UsersPageClientProps) {
   const t = useTranslations("dashboard.users");
   const tUiTable = useTranslations("ui.table");
   const tUserMgmt = useTranslations("dashboard.userManagement");
   const tKeyList = useTranslations("dashboard.keyList");
   const tCommon = useTranslations("common");
+  const hasInitialUsers = initialUsers !== undefined;
+  const {
+    data: usersData = initialUsers ?? [],
+    isLoading,
+    isFetching,
+  } = useQuery<UserDisplay[]>({
+    queryKey: ["users"],
+    queryFn: getUsers,
+    initialData: hasInitialUsers ? initialUsers : undefined,
+  });
+
+  const resolvedUsers = usersData ?? [];
+  const isInitialLoading = isLoading && resolvedUsers.length === 0;
+  const isRefreshing = isFetching && !isInitialLoading;
   const [searchTerm, setSearchTerm] = useState("");
   const [tagFilter, setTagFilter] = useState("all");
   const [keyGroupFilter, setKeyGroupFilter] = useState("all");
@@ -90,15 +123,17 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
 
   // Extract unique tags from users
   const uniqueTags = useMemo(() => {
-    const tags = users.flatMap((u) => u.tags || []);
+    const tags = resolvedUsers.flatMap((u) => u.tags || []);
     return [...new Set(tags)].sort();
-  }, [users]);
+  }, [resolvedUsers]);
 
   // Extract unique key groups from users (split comma-separated tags)
   const uniqueKeyGroups = useMemo(() => {
-    const groups = users.flatMap((u) => u.keys?.flatMap((k) => splitTags(k.providerGroup)) || []);
+    const groups = resolvedUsers.flatMap(
+      (u) => u.keys?.flatMap((k) => splitTags(k.providerGroup)) || []
+    );
     return [...new Set(groups)].sort();
-  }, [users]);
+  }, [resolvedUsers]);
 
   // Reset filter if selected value no longer exists in options
   useEffect(() => {
@@ -121,7 +156,7 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
 
     const filtered: UserDisplay[] = [];
 
-    for (const user of users) {
+    for (const user of resolvedUsers) {
       // Collect matching key IDs for this user (before filtering)
       const userMatchingKeyIds: number[] = [];
 
@@ -181,7 +216,7 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
     }
 
     return { filteredUsers: filtered, matchingKeyIds: matchingIds };
-  }, [users, searchTerm, tagFilter, keyGroupFilter]);
+  }, [resolvedUsers, searchTerm, tagFilter, keyGroupFilter]);
 
   // Determine if we should highlight keys (either search or keyGroup filter is active)
   const shouldHighlightKeys = searchTerm.trim().length > 0 || keyGroupFilter !== "all";
@@ -192,7 +227,9 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
         <div>
           <h3 className="text-lg font-medium">{t("title")}</h3>
           <p className="text-sm text-muted-foreground">
-            {t("description", { count: filteredUsers.length })}
+            {isInitialLoading
+              ? tCommon("loading")
+              : t("description", { count: filteredUsers.length })}
           </p>
         </div>
         <Button onClick={handleCreateUser}>
@@ -215,113 +252,128 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
         </div>
 
         {/* Tag filter */}
-        {uniqueTags.length > 0 && (
-          <Select value={tagFilter} onValueChange={setTagFilter}>
-            <SelectTrigger className="w-[180px]">
-              <SelectValue placeholder={t("toolbar.tagFilter")} />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value="all">{t("toolbar.allTags")}</SelectItem>
-              {uniqueTags.map((tag) => (
-                <SelectItem key={tag} value={tag}>
-                  <Badge variant="secondary" className="mr-1 text-xs">
-                    {tag}
-                  </Badge>
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
+        {isInitialLoading ? (
+          <Skeleton className="h-9 w-[180px]" />
+        ) : (
+          uniqueTags.length > 0 && (
+            <Select value={tagFilter} onValueChange={setTagFilter}>
+              <SelectTrigger className="w-[180px]">
+                <SelectValue placeholder={t("toolbar.tagFilter")} />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="all">{t("toolbar.allTags")}</SelectItem>
+                {uniqueTags.map((tag) => (
+                  <SelectItem key={tag} value={tag}>
+                    <Badge variant="secondary" className="mr-1 text-xs">
+                      {tag}
+                    </Badge>
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          )
         )}
 
         {/* Key group filter */}
-        {uniqueKeyGroups.length > 0 && (
-          <Select value={keyGroupFilter} onValueChange={setKeyGroupFilter}>
-            <SelectTrigger className="w-[180px]">
-              <SelectValue placeholder={t("toolbar.keyGroupFilter")} />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value="all">{t("toolbar.allKeyGroups")}</SelectItem>
-              {uniqueKeyGroups.map((group) => (
-                <SelectItem key={group} value={group}>
-                  <Badge variant="outline" className="mr-1 text-xs">
-                    {group}
-                  </Badge>
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
+        {isInitialLoading ? (
+          <Skeleton className="h-9 w-[180px]" />
+        ) : (
+          uniqueKeyGroups.length > 0 && (
+            <Select value={keyGroupFilter} onValueChange={setKeyGroupFilter}>
+              <SelectTrigger className="w-[180px]">
+                <SelectValue placeholder={t("toolbar.keyGroupFilter")} />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="all">{t("toolbar.allKeyGroups")}</SelectItem>
+                {uniqueKeyGroups.map((group) => (
+                  <SelectItem key={group} value={group}>
+                    <Badge variant="outline" className="mr-1 text-xs">
+                      {group}
+                    </Badge>
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          )
         )}
       </div>
 
-      <UserManagementTable
-        users={filteredUsers}
-        currentUser={currentUser}
-        currencyCode="USD"
-        onCreateUser={handleCreateUser}
-        highlightKeyIds={shouldHighlightKeys ? matchingKeyIds : undefined}
-        autoExpandOnFilter={shouldHighlightKeys}
-        translations={{
-          table: {
-            columns: {
-              username: tUserMgmt("table.columns.username"),
-              note: tUserMgmt("table.columns.note"),
-              expiresAt: tUserMgmt("table.columns.expiresAt"),
-              limit5h: tUserMgmt("table.columns.limit5h"),
-              limitDaily: tUserMgmt("table.columns.limitDaily"),
-              limitWeekly: tUserMgmt("table.columns.limitWeekly"),
-              limitMonthly: tUserMgmt("table.columns.limitMonthly"),
-              limitTotal: tUserMgmt("table.columns.limitTotal"),
-              limitSessions: tUserMgmt("table.columns.limitSessions"),
-            },
-            keyRow: {
-              fields: {
-                name: tUserMgmt("table.keyRow.name"),
-                key: tUserMgmt("table.keyRow.key"),
-                group: tUserMgmt("table.keyRow.group"),
-                todayUsage: tUserMgmt("table.keyRow.todayUsage"),
-                todayCost: tUserMgmt("table.keyRow.todayCost"),
-                lastUsed: tUserMgmt("table.keyRow.lastUsed"),
-                actions: tUserMgmt("table.keyRow.actions"),
-                callsLabel: tUserMgmt("table.keyRow.fields.callsLabel"),
-                costLabel: tUserMgmt("table.keyRow.fields.costLabel"),
+      {isInitialLoading ? (
+        <UsersTableSkeleton label={tCommon("loading")} />
+      ) : (
+        <div className="space-y-3">
+          {isRefreshing ? <InlineLoading label={tCommon("loading")} /> : null}
+          <UserManagementTable
+            users={filteredUsers}
+            currentUser={currentUser}
+            currencyCode="USD"
+            onCreateUser={handleCreateUser}
+            highlightKeyIds={shouldHighlightKeys ? matchingKeyIds : undefined}
+            autoExpandOnFilter={shouldHighlightKeys}
+            translations={{
+              table: {
+                columns: {
+                  username: tUserMgmt("table.columns.username"),
+                  note: tUserMgmt("table.columns.note"),
+                  expiresAt: tUserMgmt("table.columns.expiresAt"),
+                  limit5h: tUserMgmt("table.columns.limit5h"),
+                  limitDaily: tUserMgmt("table.columns.limitDaily"),
+                  limitWeekly: tUserMgmt("table.columns.limitWeekly"),
+                  limitMonthly: tUserMgmt("table.columns.limitMonthly"),
+                  limitTotal: tUserMgmt("table.columns.limitTotal"),
+                  limitSessions: tUserMgmt("table.columns.limitSessions"),
+                },
+                keyRow: {
+                  fields: {
+                    name: tUserMgmt("table.keyRow.name"),
+                    key: tUserMgmt("table.keyRow.key"),
+                    group: tUserMgmt("table.keyRow.group"),
+                    todayUsage: tUserMgmt("table.keyRow.todayUsage"),
+                    todayCost: tUserMgmt("table.keyRow.todayCost"),
+                    lastUsed: tUserMgmt("table.keyRow.lastUsed"),
+                    actions: tUserMgmt("table.keyRow.actions"),
+                    callsLabel: tUserMgmt("table.keyRow.fields.callsLabel"),
+                    costLabel: tUserMgmt("table.keyRow.fields.costLabel"),
+                  },
+                  actions: {
+                    details: tKeyList("detailsButton"),
+                    logs: tKeyList("logsButton"),
+                    edit: tCommon("edit"),
+                    delete: tCommon("delete"),
+                    copy: tCommon("copy"),
+                    copySuccess: tCommon("copySuccess"),
+                    copyFailed: tCommon("copyFailed"),
+                    show: tKeyList("showKeyTooltip"),
+                    hide: tKeyList("hideKeyTooltip"),
+                    quota: tUserMgmt("table.keyRow.quotaButton"),
+                  },
+                  status: {
+                    enabled: tUserMgmt("keyStatus.enabled"),
+                    disabled: tUserMgmt("keyStatus.disabled"),
+                  },
+                },
+                expand: tUserMgmt("table.expand"),
+                collapse: tUserMgmt("table.collapse"),
+                noKeys: tUserMgmt("table.noKeys"),
+                defaultGroup: tUserMgmt("table.defaultGroup"),
               },
+              editDialog: {},
               actions: {
+                edit: tCommon("edit"),
                 details: tKeyList("detailsButton"),
                 logs: tKeyList("logsButton"),
-                edit: tCommon("edit"),
                 delete: tCommon("delete"),
-                copy: tCommon("copy"),
-                copySuccess: tCommon("copySuccess"),
-                copyFailed: tCommon("copyFailed"),
-                show: tKeyList("showKeyTooltip"),
-                hide: tKeyList("hideKeyTooltip"),
-                quota: tUserMgmt("table.keyRow.quotaButton"),
               },
-              status: {
-                enabled: tUserMgmt("keyStatus.enabled"),
-                disabled: tUserMgmt("keyStatus.disabled"),
+              pagination: {
+                previous: tUiTable("previousPage"),
+                next: tUiTable("nextPage"),
+                page: "Page {page}",
+                of: "{totalPages}",
               },
-            },
-            expand: tUserMgmt("table.expand"),
-            collapse: tUserMgmt("table.collapse"),
-            noKeys: tUserMgmt("table.noKeys"),
-            defaultGroup: tUserMgmt("table.defaultGroup"),
-          },
-          editDialog: {},
-          actions: {
-            edit: tCommon("edit"),
-            details: tKeyList("detailsButton"),
-            logs: tKeyList("logsButton"),
-            delete: tCommon("delete"),
-          },
-          pagination: {
-            previous: tUiTable("previousPage"),
-            next: tUiTable("nextPage"),
-            page: "Page {page}",
-            of: "{totalPages}",
-          },
-        }}
-      />
+            }}
+          />
+        </div>
+      )}
 
       {/* Onboarding Tour */}
       <UserOnboardingTour
@@ -340,3 +392,30 @@ export function UsersPageClient({ users, currentUser }: UsersPageClientProps) {
     </div>
   );
 }
+
+function InlineLoading({ label }: { label: string }) {
+  return (
+    <div className="flex items-center gap-2 text-xs text-muted-foreground" aria-live="polite">
+      <Loader2 className="h-3 w-3 animate-spin" />
+      <span>{label}</span>
+    </div>
+  );
+}
+
+function UsersTableSkeleton({ label }: { label: string }) {
+  return (
+    <div className="rounded-xl border bg-card p-4 space-y-4" aria-busy="true">
+      <div className="grid grid-cols-5 gap-3">
+        {Array.from({ length: 5 }).map((_, index) => (
+          <Skeleton key={index} className="h-4 w-full" />
+        ))}
+      </div>
+      <div className="space-y-3">
+        {Array.from({ length: 6 }).map((_, index) => (
+          <Skeleton key={index} className="h-10 w-full" />
+        ))}
+      </div>
+      <InlineLoading label={label} />
+    </div>
+  );
+}

+ 20 - 0
src/app/[locale]/internal/dashboard/big-screen/loading.tsx

@@ -0,0 +1,20 @@
+import { LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function BigScreenLoading() {
+  return (
+    <div className="min-h-screen bg-background p-6 space-y-6">
+      <Skeleton className="h-8 w-48" />
+      <div className="grid gap-4 lg:grid-cols-4">
+        {Array.from({ length: 4 }).map((_, index) => (
+          <Skeleton key={`big-screen-card-${index}`} className="h-28 w-full" />
+        ))}
+      </div>
+      <div className="grid gap-4 lg:grid-cols-2">
+        <Skeleton className="h-64 w-full" />
+        <Skeleton className="h-64 w-full" />
+      </div>
+      <LoadingState className="text-center" />
+    </div>
+  );
+}

+ 13 - 0
src/app/[locale]/internal/data-gen/loading.tsx

@@ -0,0 +1,13 @@
+import { LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function DataGenLoading() {
+  return (
+    <div className="p-6 space-y-6">
+      <Skeleton className="h-7 w-48" />
+      <Skeleton className="h-10 w-64" />
+      <Skeleton className="h-48 w-full" />
+      <LoadingState className="text-center" />
+    </div>
+  );
+}

+ 16 - 0
src/app/[locale]/login/loading.tsx

@@ -0,0 +1,16 @@
+import { LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function LoginLoading() {
+  return (
+    <div className="flex min-h-screen items-center justify-center bg-background">
+      <div className="w-full max-w-lg space-y-4 rounded-xl border bg-card p-6">
+        <Skeleton className="h-6 w-40" />
+        <Skeleton className="h-4 w-64" />
+        <Skeleton className="h-10 w-full" />
+        <Skeleton className="h-10 w-full" />
+        <LoadingState className="text-center" />
+      </div>
+    </div>
+  );
+}

+ 44 - 0
src/app/[locale]/my-usage/_components/loading-states.test.tsx

@@ -0,0 +1,44 @@
+import type { ReactNode } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { NextIntlClientProvider } from "next-intl";
+import { describe, expect, test } from "vitest";
+import { QuotaCards } from "./quota-cards";
+import { TodayUsageCard } from "./today-usage-card";
+
+const messages = {
+  myUsage: {
+    quota: {},
+    expiration: {},
+    today: {
+      title: "Today",
+      autoRefresh: "Auto refresh {seconds}s",
+      refresh: "Refresh",
+      modelBreakdown: "Model breakdown",
+    },
+  },
+  common: {
+    loading: "Loading...",
+  },
+};
+
+function renderWithIntl(node: ReactNode) {
+  return renderToStaticMarkup(
+    <NextIntlClientProvider locale="en" messages={messages}>
+      {node}
+    </NextIntlClientProvider>
+  );
+}
+
+describe("my-usage loading states", () => {
+  test("QuotaCards renders skeletons and loading label when loading", () => {
+    const html = renderWithIntl(<QuotaCards quota={null} loading />);
+    expect(html).toContain("Loading...");
+    expect(html).toContain('data-slot="skeleton"');
+  });
+
+  test("TodayUsageCard renders skeletons and loading label when loading", () => {
+    const html = renderWithIntl(<TodayUsageCard stats={null} loading autoRefreshSeconds={30} />);
+    expect(html).toContain("Loading...");
+    expect(html).toContain('data-slot="skeleton"');
+  });
+});

+ 32 - 0
src/app/[locale]/my-usage/_components/quota-cards.tsx

@@ -6,6 +6,7 @@ import type { MyUsageQuota } from "@/actions/my-usage";
 import { QuotaCountdownCompact } from "@/components/quota/quota-countdown";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Progress } from "@/components/ui/progress";
+import { Skeleton } from "@/components/ui/skeleton";
 import { useCountdown } from "@/hooks/useCountdown";
 import type { CurrencyCode } from "@/lib/utils";
 import { cn } from "@/lib/utils";
@@ -27,6 +28,11 @@ export function QuotaCards({
 }: QuotaCardsProps) {
   const t = useTranslations("myUsage.quota");
   const tExpiration = useTranslations("myUsage.expiration");
+  const tCommon = useTranslations("common");
+
+  if (loading && !quota) {
+    return <QuotaCardsSkeleton label={tCommon("loading")} />;
+  }
 
   const resolvedKeyExpires = keyExpiresAt ?? quota?.expiresAt ?? null;
   const resolvedUserExpires = userExpiresAt ?? quota?.userExpiresAt ?? null;
@@ -192,6 +198,32 @@ export function QuotaCards({
   );
 }
 
+function QuotaCardsSkeleton({ label }: { label: string }) {
+  return (
+    <div className="space-y-3" aria-busy="true">
+      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
+        {Array.from({ length: 6 }).map((_, index) => (
+          <Card key={index} className="border-border/70">
+            <CardHeader className="pb-3">
+              <Skeleton className="h-4 w-20" />
+            </CardHeader>
+            <CardContent className="space-y-3">
+              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+                <Skeleton className="h-16 w-full" />
+                <Skeleton className="h-16 w-full" />
+              </div>
+            </CardContent>
+          </Card>
+        ))}
+      </div>
+      <div className="flex items-center gap-2 text-xs text-muted-foreground">
+        <Skeleton className="h-3 w-3 rounded-full" />
+        <span>{label}</span>
+      </div>
+    </div>
+  );
+}
+
 function QuotaColumn({
   label,
   current,

+ 43 - 13
src/app/[locale]/my-usage/_components/today-usage-card.tsx

@@ -1,15 +1,17 @@
 "use client";
 
-import { RefreshCw } from "lucide-react";
+import { Loader2, RefreshCw } from "lucide-react";
 import { useTranslations } from "next-intl";
 import type { MyTodayStats } from "@/actions/my-usage";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
 
 interface TodayUsageCardProps {
   stats: MyTodayStats | null;
   loading?: boolean;
+  refreshing?: boolean;
   onRefresh?: () => void;
   autoRefreshSeconds?: number;
 }
@@ -17,10 +19,14 @@ interface TodayUsageCardProps {
 export function TodayUsageCard({
   stats,
   loading = false,
+  refreshing = false,
   onRefresh,
   autoRefreshSeconds = 30,
 }: TodayUsageCardProps) {
   const t = useTranslations("myUsage.today");
+  const tCommon = useTranslations("common");
+  const isInitialLoading = loading && !stats;
+  const isButtonLoading = loading || refreshing;
 
   return (
     <Card>
@@ -33,29 +39,53 @@ export function TodayUsageCard({
             variant="outline"
             className="h-8 gap-2"
             onClick={onRefresh}
-            disabled={loading}
+            disabled={isButtonLoading}
           >
-            <RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />
+            <RefreshCw className={`h-3.5 w-3.5 ${isButtonLoading ? "animate-spin" : ""}`} />
             {t("refresh")}
           </Button>
         </div>
       </CardHeader>
       <CardContent className="space-y-4">
-        <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
-          <Metric label={t("calls")} value={stats?.calls ?? 0} />
-          <Metric label={t("tokensIn")} value={stats?.inputTokens ?? 0} />
-          <Metric label={t("tokensOut")} value={stats?.outputTokens ?? 0} />
-          <Metric
-            label={t("cost", { currency: stats?.currencyCode ?? "USD" })}
-            value={Number(stats?.costUsd ?? 0).toFixed(4)}
-          />
-        </div>
+        {isInitialLoading ? (
+          <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
+            {Array.from({ length: 4 }).map((_, index) => (
+              <div key={index} className="rounded-lg border bg-card/50 px-3 py-2 space-y-2">
+                <Skeleton className="h-3 w-16" />
+                <Skeleton className="h-5 w-20" />
+              </div>
+            ))}
+          </div>
+        ) : (
+          <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
+            <Metric label={t("calls")} value={stats?.calls ?? 0} />
+            <Metric label={t("tokensIn")} value={stats?.inputTokens ?? 0} />
+            <Metric label={t("tokensOut")} value={stats?.outputTokens ?? 0} />
+            <Metric
+              label={t("cost", { currency: stats?.currencyCode ?? "USD" })}
+              value={Number(stats?.costUsd ?? 0).toFixed(4)}
+            />
+          </div>
+        )}
 
         <Separator />
 
         <div className="space-y-2">
           <p className="text-sm font-medium text-muted-foreground">{t("modelBreakdown")}</p>
-          {stats && stats.modelBreakdown.length > 0 ? (
+          {isInitialLoading ? (
+            <div className="space-y-2">
+              {Array.from({ length: 3 }).map((_, index) => (
+                <div key={index} className="rounded-md border px-3 py-2 space-y-2">
+                  <Skeleton className="h-4 w-32" />
+                  <Skeleton className="h-3 w-48" />
+                </div>
+              ))}
+              <div className="flex items-center gap-2 text-xs text-muted-foreground">
+                <Loader2 className="h-3 w-3 animate-spin" />
+                <span>{tCommon("loading")}</span>
+              </div>
+            </div>
+          ) : stats && stats.modelBreakdown.length > 0 ? (
             <div className="space-y-2">
               {stats.modelBreakdown.map((item) => (
                 <div

+ 52 - 19
src/app/[locale]/my-usage/_components/usage-logs-section.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { Loader2 } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useCallback, useEffect, useRef, useState, useTransition } from "react";
 import {
@@ -24,6 +25,7 @@ import { UsageLogsTable } from "./usage-logs-table";
 
 interface UsageLogsSectionProps {
   initialData?: MyUsageLogsResult | null;
+  loading?: boolean;
   autoRefreshSeconds?: number;
 }
 
@@ -40,31 +42,40 @@ interface Filters {
 
 export function UsageLogsSection({
   initialData = null,
+  loading = false,
   autoRefreshSeconds,
 }: UsageLogsSectionProps) {
   const t = useTranslations("myUsage.logs");
   const tDashboard = useTranslations("dashboard");
+  const tCommon = useTranslations("common");
   const [models, setModels] = useState<string[]>([]);
   const [endpoints, setEndpoints] = useState<string[]>([]);
+  const [isModelsLoading, setIsModelsLoading] = useState(true);
+  const [isEndpointsLoading, setIsEndpointsLoading] = useState(true);
   const [filters, setFilters] = useState<Filters>({ page: 1 });
   const [data, setData] = useState<MyUsageLogsResult | null>(initialData);
   const [isPending, startTransition] = useTransition();
   const [error, setError] = useState<string | null>(null);
 
   useEffect(() => {
-    const loadOptions = async () => {
-      const [modelsResult, endpointsResult] = await Promise.all([
-        getMyAvailableModels(),
-        getMyAvailableEndpoints(),
-      ]);
-      if (modelsResult.ok && modelsResult.data) {
-        setModels(modelsResult.data);
-      }
-      if (endpointsResult.ok && endpointsResult.data) {
-        setEndpoints(endpointsResult.data);
-      }
-    };
-    loadOptions();
+    setIsModelsLoading(true);
+    setIsEndpointsLoading(true);
+
+    void getMyAvailableModels()
+      .then((modelsResult) => {
+        if (modelsResult.ok && modelsResult.data) {
+          setModels(modelsResult.data);
+        }
+      })
+      .finally(() => setIsModelsLoading(false));
+
+    void getMyAvailableEndpoints()
+      .then((endpointsResult) => {
+        if (endpointsResult.ok && endpointsResult.data) {
+          setEndpoints(endpointsResult.data);
+        }
+      })
+      .finally(() => setIsEndpointsLoading(false));
   }, []);
 
   const loadLogs = useCallback(
@@ -86,10 +97,10 @@ export function UsageLogsSection({
 
   useEffect(() => {
     // initial load if not provided
-    if (!initialData) {
+    if (!initialData && !loading) {
       loadLogs(true);
     }
-  }, [initialData, loadLogs]);
+  }, [initialData, loading, loadLogs]);
 
   // Auto-refresh polling (only when on page 1 to avoid disrupting history browsing)
   const intervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -168,6 +179,9 @@ export function UsageLogsSection({
     });
   };
 
+  const isInitialLoading = loading || (!data && isPending);
+  const isRefreshing = isPending && Boolean(data);
+
   return (
     <Card>
       <CardHeader className="flex flex-row items-center justify-between">
@@ -199,9 +213,12 @@ export function UsageLogsSection({
                   model: value === "__all__" ? undefined : value,
                 })
               }
+              disabled={isModelsLoading}
             >
               <SelectTrigger>
-                <SelectValue placeholder={t("filters.allModels")} />
+                <SelectValue
+                  placeholder={isModelsLoading ? tCommon("loading") : t("filters.allModels")}
+                />
               </SelectTrigger>
               <SelectContent>
                 <SelectItem value="__all__">{t("filters.allModels")}</SelectItem>
@@ -222,9 +239,16 @@ export function UsageLogsSection({
                   endpoint: value === "__all__" ? undefined : value,
                 })
               }
+              disabled={isEndpointsLoading}
             >
               <SelectTrigger>
-                <SelectValue placeholder={tDashboard("logs.filters.allEndpoints")} />
+                <SelectValue
+                  placeholder={
+                    isEndpointsLoading
+                      ? tCommon("loading")
+                      : tDashboard("logs.filters.allEndpoints")
+                  }
+                />
               </SelectTrigger>
               <SelectContent>
                 <SelectItem value="__all__">{tDashboard("logs.filters.allEndpoints")}</SelectItem>
@@ -284,16 +308,23 @@ export function UsageLogsSection({
         </div>
 
         <div className="flex items-center gap-2">
-          <Button size="sm" onClick={handleApply} disabled={isPending}>
+          <Button size="sm" onClick={handleApply} disabled={isPending || loading}>
             {t("filters.apply")}
           </Button>
-          <Button size="sm" variant="outline" onClick={handleReset} disabled={isPending}>
+          <Button size="sm" variant="outline" onClick={handleReset} disabled={isPending || loading}>
             {t("filters.reset")}
           </Button>
         </div>
 
         {error ? <p className="text-sm text-destructive">{error}</p> : null}
 
+        {isRefreshing ? (
+          <div className="flex items-center gap-2 text-xs text-muted-foreground">
+            <Loader2 className="h-3 w-3 animate-spin" />
+            <span>{tCommon("loading")}</span>
+          </div>
+        ) : null}
+
         <UsageLogsTable
           logs={data?.logs ?? []}
           total={data?.total ?? 0}
@@ -301,6 +332,8 @@ export function UsageLogsSection({
           pageSize={data?.pageSize ?? 20}
           onPageChange={handlePageChange}
           currencyCode={data?.currencyCode}
+          loading={isInitialLoading}
+          loadingLabel={tCommon("loading")}
         />
       </CardContent>
     </Card>

+ 25 - 8
src/app/[locale]/my-usage/_components/usage-logs-table.tsx

@@ -3,6 +3,7 @@
 import { useTranslations } from "next-intl";
 import type { MyUsageLogEntry } from "@/actions/my-usage";
 import { Badge } from "@/components/ui/badge";
+import { Skeleton } from "@/components/ui/skeleton";
 import {
   Table,
   TableBody,
@@ -21,6 +22,8 @@ interface UsageLogsTableProps {
   pageSize: number;
   onPageChange: (page: number) => void;
   currencyCode?: CurrencyCode;
+  loading?: boolean;
+  loadingLabel?: string;
 }
 
 export function UsageLogsTable({
@@ -30,6 +33,8 @@ export function UsageLogsTable({
   pageSize,
   onPageChange,
   currencyCode = "USD",
+  loading = false,
+  loadingLabel,
 }: UsageLogsTableProps) {
   const t = useTranslations("myUsage.logs");
   const totalPages = Math.max(1, Math.ceil(total / pageSize));
@@ -58,7 +63,17 @@ export function UsageLogsTable({
             </TableRow>
           </TableHeader>
           <TableBody>
-            {logs.length === 0 ? (
+            {loading ? (
+              Array.from({ length: 6 }).map((_, rowIndex) => (
+                <TableRow key={`skeleton-${rowIndex}`}>
+                  {Array.from({ length: 8 }).map((_, cellIndex) => (
+                    <TableCell key={`skeleton-${rowIndex}-${cellIndex}`}>
+                      <Skeleton className="h-4 w-full" />
+                    </TableCell>
+                  ))}
+                </TableRow>
+              ))
+            ) : logs.length === 0 ? (
               <TableRow>
                 <TableCell colSpan={8} className="text-center text-muted-foreground">
                   {t("noLogs")}
@@ -131,17 +146,19 @@ export function UsageLogsTable({
 
       <div className="flex items-center justify-between text-sm text-muted-foreground">
         <span>
-          {t("pagination", {
-            from: (page - 1) * pageSize + 1,
-            to: Math.min(page * pageSize, total),
-            total,
-          })}
+          {loading && loadingLabel
+            ? loadingLabel
+            : t("pagination", {
+                from: (page - 1) * pageSize + 1,
+                to: Math.min(page * pageSize, total),
+                total,
+              })}
         </span>
         <div className="flex items-center gap-2">
           <button
             className="rounded-md border px-3 py-1 text-xs disabled:opacity-50"
             onClick={() => onPageChange(Math.max(1, page - 1))}
-            disabled={page <= 1}
+            disabled={page <= 1 || loading}
           >
             {t("prev")}
           </button>
@@ -151,7 +168,7 @@ export function UsageLogsTable({
           <button
             className="rounded-md border px-3 py-1 text-xs disabled:opacity-50"
             onClick={() => onPageChange(Math.min(totalPages, page + 1))}
-            disabled={page >= totalPages}
+            disabled={page >= totalPages || loading}
           >
             {t("next")}
           </button>

+ 25 - 0
src/app/[locale]/my-usage/loading.tsx

@@ -0,0 +1,25 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function MyUsageLoading() {
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div className="space-y-2">
+          <Skeleton className="h-6 w-40" />
+          <Skeleton className="h-4 w-64" />
+        </div>
+        <Skeleton className="h-9 w-24" />
+      </div>
+      <div className="grid gap-4 md:grid-cols-2">
+        <Skeleton className="h-28 w-full" />
+        <Skeleton className="h-28 w-full" />
+      </div>
+      <Skeleton className="h-32 w-full" />
+      <div className="rounded-xl border bg-card p-4 space-y-4">
+        <TableSkeleton rows={6} columns={5} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 32 - 18
src/app/[locale]/my-usage/page.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useCallback, useEffect, useRef, useState, useTransition } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
 import {
   getMyQuota,
   getMyTodayStats,
@@ -23,29 +23,42 @@ export default function MyUsagePage() {
   const [quota, setQuota] = useState<MyUsageQuota | null>(null);
   const [todayStats, setTodayStats] = useState<MyTodayStats | null>(null);
   const [logsData, setLogsData] = useState<MyUsageLogsResult | null>(null);
-  const [isPending, startTransition] = useTransition();
-  const [hasLoaded, setHasLoaded] = useState(false);
+  const [isQuotaLoading, setIsQuotaLoading] = useState(true);
+  const [isStatsLoading, setIsStatsLoading] = useState(true);
+  const [isLogsLoading, setIsLogsLoading] = useState(true);
+  const [isStatsRefreshing, setIsStatsRefreshing] = useState(false);
 
   const intervalRef = useRef<NodeJS.Timeout | null>(null);
 
   const loadInitial = useCallback(() => {
-    startTransition(async () => {
-      const [quotaResult, statsResult, logsResult] = await Promise.all([
-        getMyQuota(),
-        getMyTodayStats(),
-        getMyUsageLogs({ page: 1 }),
-      ]);
-
-      if (quotaResult.ok) setQuota(quotaResult.data);
-      if (statsResult.ok) setTodayStats(statsResult.data);
-      if (logsResult.ok) setLogsData(logsResult.data ?? null);
-      setHasLoaded(true);
-    });
+    setIsQuotaLoading(true);
+    setIsStatsLoading(true);
+    setIsLogsLoading(true);
+
+    void getMyQuota()
+      .then((quotaResult) => {
+        if (quotaResult.ok) setQuota(quotaResult.data);
+      })
+      .finally(() => setIsQuotaLoading(false));
+
+    void getMyTodayStats()
+      .then((statsResult) => {
+        if (statsResult.ok) setTodayStats(statsResult.data);
+      })
+      .finally(() => setIsStatsLoading(false));
+
+    void getMyUsageLogs({ page: 1 })
+      .then((logsResult) => {
+        if (logsResult.ok) setLogsData(logsResult.data ?? null);
+      })
+      .finally(() => setIsLogsLoading(false));
   }, []);
 
   const refreshToday = useCallback(async () => {
+    setIsStatsRefreshing(true);
     const stats = await getMyTodayStats();
     if (stats.ok) setTodayStats(stats.data);
+    setIsStatsRefreshing(false);
   }, []);
 
   useEffect(() => {
@@ -116,7 +129,7 @@ export default function MyUsagePage() {
 
       <QuotaCards
         quota={quota}
-        loading={!hasLoaded || isPending}
+        loading={isQuotaLoading}
         currencyCode={currencyCode}
         keyExpiresAt={keyExpiresAt}
         userExpiresAt={userExpiresAt}
@@ -134,12 +147,13 @@ export default function MyUsagePage() {
 
       <TodayUsageCard
         stats={todayStats}
-        loading={!hasLoaded || isPending}
+        loading={isStatsLoading}
+        refreshing={isStatsRefreshing}
         onRefresh={refreshToday}
         autoRefreshSeconds={30}
       />
 
-      <UsageLogsSection initialData={logsData} autoRefreshSeconds={30} />
+      <UsageLogsSection initialData={logsData} loading={isLogsLoading} autoRefreshSeconds={30} />
     </div>
   );
 }

+ 37 - 0
src/app/[locale]/settings/client-versions/_components/client-versions-skeleton.tsx

@@ -0,0 +1,37 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ClientVersionsSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <Skeleton className="h-5 w-40" />
+        <Skeleton className="h-4 w-56" />
+        <Skeleton className="h-10 w-32" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-4">
+        <Skeleton className="h-5 w-40" />
+        <TableSkeleton rows={6} columns={4} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}
+
+export function ClientVersionsSettingsSkeleton() {
+  return (
+    <div className="space-y-3" aria-busy="true">
+      <Skeleton className="h-10 w-32" />
+      <LoadingState />
+    </div>
+  );
+}
+
+export function ClientVersionsTableSkeleton() {
+  return (
+    <div className="space-y-4" aria-busy="true">
+      <TableSkeleton rows={6} columns={4} />
+      <LoadingState />
+    </div>
+  );
+}

+ 39 - 22
src/app/[locale]/settings/client-versions/page.tsx

@@ -1,4 +1,5 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { fetchClientVersionStats } from "@/actions/client-versions";
 import { fetchSystemSettings } from "@/actions/system-config";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -7,6 +8,10 @@ import { getSession } from "@/lib/auth";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { ClientVersionStatsTable } from "./_components/client-version-stats-table";
 import { ClientVersionToggle } from "./_components/client-version-toggle";
+import {
+  ClientVersionsSettingsSkeleton,
+  ClientVersionsTableSkeleton,
+} from "./_components/client-versions-skeleton";
 
 export default async function ClientVersionsPage({
   params,
@@ -23,23 +28,12 @@ export default async function ClientVersionsPage({
     return redirect({ href: "/login", locale });
   }
 
-  const [statsResult, settingsResult] = await Promise.all([
-    fetchClientVersionStats(),
-    fetchSystemSettings(),
-  ]);
-
-  const stats = statsResult.ok ? statsResult.data : [];
-  const enableClientVersionCheck = settingsResult.ok
-    ? settingsResult.data.enableClientVersionCheck
-    : false;
-
   return (
     <div className="space-y-6">
       <SettingsPageHeader
         title={t("clientVersions.title")}
         description={t("clientVersions.description")}
       />
-
       {/* 功能开关和说明 */}
       <Card>
         <CardHeader>
@@ -47,7 +41,9 @@ export default async function ClientVersionsPage({
           <CardDescription>{t("clientVersions.section.settings.description")}</CardDescription>
         </CardHeader>
         <CardContent>
-          <ClientVersionToggle enabled={enableClientVersionCheck} />
+          <Suspense fallback={<ClientVersionsSettingsSkeleton />}>
+            <ClientVersionsSettingsContent />
+          </Suspense>
         </CardContent>
       </Card>
 
@@ -58,18 +54,39 @@ export default async function ClientVersionsPage({
           <CardDescription>{t("clientVersions.section.distribution.description")}</CardDescription>
         </CardHeader>
         <CardContent>
-          {stats && stats.length > 0 ? (
-            <ClientVersionStatsTable data={stats} />
-          ) : (
-            <div className="flex flex-col items-center justify-center py-12 text-center">
-              <p className="text-muted-foreground">{t("clientVersions.empty.title")}</p>
-              <p className="mt-2 text-sm text-muted-foreground">
-                {t("clientVersions.empty.description")}
-              </p>
-            </div>
-          )}
+          <Suspense fallback={<ClientVersionsTableSkeleton />}>
+            <ClientVersionsStatsContent />
+          </Suspense>
         </CardContent>
       </Card>
     </div>
   );
 }
+
+async function ClientVersionsSettingsContent() {
+  const settingsResult = await fetchSystemSettings();
+  const enableClientVersionCheck = settingsResult.ok
+    ? settingsResult.data.enableClientVersionCheck
+    : false;
+
+  return <ClientVersionToggle enabled={enableClientVersionCheck} />;
+}
+
+async function ClientVersionsStatsContent() {
+  const t = await getTranslations("settings");
+  const statsResult = await fetchClientVersionStats();
+  const stats = statsResult.ok ? statsResult.data : [];
+
+  if (!stats || stats.length === 0) {
+    return (
+      <div className="flex flex-col items-center justify-center py-12 text-center">
+        <p className="text-muted-foreground">{t("clientVersions.empty.title")}</p>
+        <p className="mt-2 text-sm text-muted-foreground">
+          {t("clientVersions.empty.description")}
+        </p>
+      </div>
+    );
+  }
+
+  return <ClientVersionStatsTable data={stats} />;
+}

+ 11 - 0
src/app/[locale]/settings/config/_components/settings-config-skeleton.tsx

@@ -0,0 +1,11 @@
+import { LoadingState, SectionSkeleton } from "@/components/loading/page-skeletons";
+
+export function SettingsConfigSkeleton() {
+  return (
+    <div className="space-y-6">
+      <SectionSkeleton rows={4} />
+      <SectionSkeleton rows={3} />
+      <LoadingState className="text-center" />
+    </div>
+  );
+}

+ 14 - 1
src/app/[locale]/settings/config/page.tsx

@@ -1,20 +1,33 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { Section } from "@/components/section";
 import { getSystemSettings } from "@/repository/system-config";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { AutoCleanupForm } from "./_components/auto-cleanup-form";
+import { SettingsConfigSkeleton } from "./_components/settings-config-skeleton";
 import { SystemSettingsForm } from "./_components/system-settings-form";
 
 export const dynamic = "force-dynamic";
 
 export default async function SettingsConfigPage() {
   const t = await getTranslations("settings");
-  const settings = await getSystemSettings();
 
   return (
     <>
       <SettingsPageHeader title={t("config.title")} description={t("config.description")} />
+      <Suspense fallback={<SettingsConfigSkeleton />}>
+        <SettingsConfigContent />
+      </Suspense>
+    </>
+  );
+}
 
+async function SettingsConfigContent() {
+  const t = await getTranslations("settings");
+  const settings = await getSystemSettings();
+
+  return (
+    <>
       <Section
         title={t("config.section.siteParams.title")}
         description={t("config.section.siteParams.description")}

+ 34 - 0
src/app/[locale]/settings/error-rules/_components/error-rules-skeleton.tsx

@@ -0,0 +1,34 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ErrorRulesSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <Skeleton className="h-5 w-40" />
+        <Skeleton className="h-4 w-56" />
+        <Skeleton className="h-24 w-full" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-4">
+        <div className="flex items-center justify-between">
+          <Skeleton className="h-5 w-36" />
+          <div className="flex gap-2">
+            <Skeleton className="h-9 w-24" />
+            <Skeleton className="h-9 w-24" />
+          </div>
+        </div>
+        <TableSkeleton rows={6} columns={4} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}
+
+export function ErrorRulesTableSkeleton() {
+  return (
+    <div className="rounded-xl border bg-card p-5 space-y-4" aria-busy="true">
+      <TableSkeleton rows={6} columns={4} />
+      <LoadingState />
+    </div>
+  );
+}

+ 20 - 5
src/app/[locale]/settings/error-rules/page.tsx

@@ -1,9 +1,12 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { getCacheStats, listErrorRules } from "@/actions/error-rules";
 import { Section } from "@/components/section";
+import { Skeleton } from "@/components/ui/skeleton";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { AddRuleDialog } from "./_components/add-rule-dialog";
 import { ErrorRuleTester } from "./_components/error-rule-tester";
+import { ErrorRulesTableSkeleton } from "./_components/error-rules-skeleton";
 import { RefreshCacheButton } from "./_components/refresh-cache-button";
 import { RuleListTable } from "./_components/rule-list-table";
 
@@ -11,12 +14,10 @@ export const dynamic = "force-dynamic";
 
 export default async function ErrorRulesPage() {
   const t = await getTranslations("settings");
-  const [rules, cacheStats] = await Promise.all([listErrorRules(), getCacheStats()]);
 
   return (
     <>
       <SettingsPageHeader title={t("errorRules.title")} description={t("errorRules.description")} />
-
       <div className="space-y-6">
         <Section
           title={t("errorRules.tester.title")}
@@ -26,17 +27,31 @@ export default async function ErrorRulesPage() {
         </Section>
 
         <Section
-          title={`${t("errorRules.section.title")} (${rules.length})`}
+          title={t("errorRules.section.title")}
           actions={
             <div className="flex gap-2">
-              <RefreshCacheButton stats={cacheStats} />
+              <Suspense fallback={<Skeleton className="h-9 w-24" />}>
+                <ErrorRulesRefreshAction />
+              </Suspense>
               <AddRuleDialog />
             </div>
           }
         >
-          <RuleListTable rules={rules} />
+          <Suspense fallback={<ErrorRulesTableSkeleton />}>
+            <ErrorRulesTableContent />
+          </Suspense>
         </Section>
       </div>
     </>
   );
 }
+
+async function ErrorRulesRefreshAction() {
+  const cacheStats = await getCacheStats();
+  return <RefreshCacheButton stats={cacheStats} />;
+}
+
+async function ErrorRulesTableContent() {
+  const rules = await listErrorRules();
+  return <RuleListTable rules={rules} />;
+}

+ 16 - 0
src/app/[locale]/settings/loading.tsx

@@ -0,0 +1,16 @@
+import {
+  LoadingState,
+  PageHeaderSkeleton,
+  SectionSkeleton,
+} from "@/components/loading/page-skeletons";
+
+export default function SettingsLoading() {
+  return (
+    <div className="space-y-6">
+      <PageHeaderSkeleton titleWidth="w-44" descriptionWidth="w-72" />
+      <SectionSkeleton rows={4} />
+      <SectionSkeleton rows={3} />
+      <LoadingState className="text-center" />
+    </div>
+  );
+}

+ 10 - 3
src/app/[locale]/settings/logs/_components/log-level-form.tsx

@@ -17,8 +17,10 @@ type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
 
 export function LogLevelForm() {
   const t = useTranslations("settings.logs");
+  const tCommon = useTranslations("settings.common");
   const [currentLevel, setCurrentLevel] = useState<LogLevel>("info");
   const [selectedLevel, setSelectedLevel] = useState<LogLevel>("info");
+  const [isLoading, setIsLoading] = useState(true);
   const [isPending, startTransition] = useTransition();
 
   const LOG_LEVELS: { value: LogLevel; label: string; description: string }[] = [
@@ -31,6 +33,7 @@ export function LogLevelForm() {
   ];
 
   useEffect(() => {
+    setIsLoading(true);
     fetch("/api/admin/log-level")
       .then((res) => res.json())
       .then((data) => {
@@ -39,6 +42,9 @@ export function LogLevelForm() {
       })
       .catch(() => {
         toast.error(t("form.fetchFailed"));
+      })
+      .finally(() => {
+        setIsLoading(false);
       });
   }, [t]);
 
@@ -76,8 +82,8 @@ export function LogLevelForm() {
           value={selectedLevel}
           onValueChange={(value) => setSelectedLevel(value as LogLevel)}
         >
-          <SelectTrigger id="log-level" disabled={isPending}>
-            <SelectValue />
+          <SelectTrigger id="log-level" disabled={isPending || isLoading}>
+            <SelectValue placeholder={isLoading ? tCommon("loading") : t("form.selectLevel")} />
           </SelectTrigger>
           <SelectContent>
             {LOG_LEVELS.map((level) => (
@@ -90,6 +96,7 @@ export function LogLevelForm() {
             ))}
           </SelectContent>
         </Select>
+        {isLoading ? <p className="text-xs text-muted-foreground">{tCommon("loading")}</p> : null}
         <p className="text-xs text-muted-foreground">{t("form.effectiveImmediately")}</p>
       </div>
 
@@ -116,7 +123,7 @@ export function LogLevelForm() {
       )}
 
       <div className="flex justify-end">
-        <Button type="submit" disabled={isPending || selectedLevel === currentLevel}>
+        <Button type="submit" disabled={isPending || isLoading || selectedLevel === currentLevel}>
           {isPending ? t("form.saving") : t("form.save")}
         </Button>
       </div>

+ 285 - 284
src/app/[locale]/settings/notifications/page.tsx

@@ -17,6 +17,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
 import { Slider } from "@/components/ui/slider";
 import { Switch } from "@/components/ui/switch";
 
@@ -152,14 +153,6 @@ export default function NotificationsPage() {
     }
   };
 
-  if (isLoading) {
-    return (
-      <div className="flex items-center justify-center min-h-[400px]">
-        <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
-      </div>
-    );
-  }
-
   return (
     <div className="space-y-6">
       <div>
@@ -167,308 +160,316 @@ export default function NotificationsPage() {
         <p className="text-muted-foreground mt-2">{t("notifications.description")}</p>
       </div>
 
-      <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
-        {/* 全局开关 */}
-        <Card>
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <Bell className="w-5 h-5" />
-              {t("notifications.global.title")}
-            </CardTitle>
-            <CardDescription>{t("notifications.global.description")}</CardDescription>
-          </CardHeader>
-          <CardContent>
-            <div className="flex items-center justify-between">
-              <Label htmlFor="enabled">{t("notifications.global.enable")}</Label>
-              <Switch
-                id="enabled"
-                checked={enabled}
-                onCheckedChange={(checked) => setValue("enabled", checked)}
-              />
-            </div>
-          </CardContent>
-        </Card>
+      {isLoading ? (
+        <NotificationsSkeleton label={t("common.loading")} />
+      ) : (
+        <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
+          {/* 全局开关 */}
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <Bell className="w-5 h-5" />
+                {t("notifications.global.title")}
+              </CardTitle>
+              <CardDescription>{t("notifications.global.description")}</CardDescription>
+            </CardHeader>
+            <CardContent>
+              <div className="flex items-center justify-between">
+                <Label htmlFor="enabled">{t("notifications.global.enable")}</Label>
+                <Switch
+                  id="enabled"
+                  checked={enabled}
+                  onCheckedChange={(checked) => setValue("enabled", checked)}
+                />
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* 熔断器告警配置 */}
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <AlertTriangle className="w-5 h-5 text-red-500" />
+                {t("notifications.circuitBreaker.title")}
+              </CardTitle>
+              <CardDescription>{t("notifications.circuitBreaker.description")}</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <div className="flex items-center justify-between">
+                <Label htmlFor="circuitBreakerEnabled">
+                  {t("notifications.circuitBreaker.enable")}
+                </Label>
+                <Switch
+                  id="circuitBreakerEnabled"
+                  checked={circuitBreakerEnabled}
+                  disabled={!enabled}
+                  onCheckedChange={(checked) => setValue("circuitBreakerEnabled", checked)}
+                />
+              </div>
 
-        {/* 熔断器告警配置 */}
-        <Card>
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <AlertTriangle className="w-5 h-5 text-red-500" />
-              {t("notifications.circuitBreaker.title")}
-            </CardTitle>
-            <CardDescription>{t("notifications.circuitBreaker.description")}</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-4">
-            <div className="flex items-center justify-between">
-              <Label htmlFor="circuitBreakerEnabled">
-                {t("notifications.circuitBreaker.enable")}
-              </Label>
-              <Switch
-                id="circuitBreakerEnabled"
-                checked={circuitBreakerEnabled}
-                disabled={!enabled}
-                onCheckedChange={(checked) => setValue("circuitBreakerEnabled", checked)}
-              />
-            </div>
-
-            {circuitBreakerEnabled && (
-              <div className="space-y-4 pt-4">
-                <Separator />
-                <div className="space-y-2">
-                  <Label htmlFor="circuitBreakerWebhook">
-                    {t("notifications.circuitBreaker.webhook")}
-                  </Label>
-                  <Input
-                    id="circuitBreakerWebhook"
-                    {...register("circuitBreakerWebhook")}
-                    placeholder={t("notifications.circuitBreaker.webhookPlaceholder")}
-                    disabled={!enabled}
-                  />
-                  {errors.circuitBreakerWebhook && (
-                    <p className="text-sm text-red-500">{errors.circuitBreakerWebhook.message}</p>
-                  )}
-                </div>
+              {circuitBreakerEnabled && (
+                <div className="space-y-4 pt-4">
+                  <Separator />
+                  <div className="space-y-2">
+                    <Label htmlFor="circuitBreakerWebhook">
+                      {t("notifications.circuitBreaker.webhook")}
+                    </Label>
+                    <Input
+                      id="circuitBreakerWebhook"
+                      {...register("circuitBreakerWebhook")}
+                      placeholder={t("notifications.circuitBreaker.webhookPlaceholder")}
+                      disabled={!enabled}
+                    />
+                    {errors.circuitBreakerWebhook && (
+                      <p className="text-sm text-red-500">{errors.circuitBreakerWebhook.message}</p>
+                    )}
+                  </div>
 
-                <Button
-                  type="button"
-                  variant="outline"
-                  size="sm"
-                  disabled={!enabled || testingWebhook === "circuitBreaker"}
-                  onClick={() =>
-                    handleTestWebhook(watch("circuitBreakerWebhook") || "", "circuitBreaker")
-                  }
-                >
-                  {testingWebhook === "circuitBreaker" ? (
-                    <>
-                      <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                      {t("common.testing")}
-                    </>
-                  ) : (
-                    <>
-                      <TestTube className="w-4 h-4 mr-2" />
-                      {t("notifications.circuitBreaker.test")}
-                    </>
-                  )}
-                </Button>
+                  <Button
+                    type="button"
+                    variant="outline"
+                    size="sm"
+                    disabled={!enabled || testingWebhook === "circuitBreaker"}
+                    onClick={() =>
+                      handleTestWebhook(watch("circuitBreakerWebhook") || "", "circuitBreaker")
+                    }
+                  >
+                    {testingWebhook === "circuitBreaker" ? (
+                      <>
+                        <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                        {t("common.testing")}
+                      </>
+                    ) : (
+                      <>
+                        <TestTube className="w-4 h-4 mr-2" />
+                        {t("notifications.circuitBreaker.test")}
+                      </>
+                    )}
+                  </Button>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+
+          {/* 每日排行榜配置 */}
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <TrendingUp className="w-5 h-5 text-green-600" />
+                {t("notifications.dailyLeaderboard.title")}
+              </CardTitle>
+              <CardDescription>{t("notifications.dailyLeaderboard.description")}</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <div className="flex items-center justify-between">
+                <Label htmlFor="dailyLeaderboardEnabled">
+                  {t("notifications.dailyLeaderboard.enable")}
+                </Label>
+                <Switch
+                  id="dailyLeaderboardEnabled"
+                  checked={dailyLeaderboardEnabled}
+                  disabled={!enabled}
+                  onCheckedChange={(checked) => setValue("dailyLeaderboardEnabled", checked)}
+                />
               </div>
-            )}
-          </CardContent>
-        </Card>
 
-        {/* 每日排行榜配置 */}
-        <Card>
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <TrendingUp className="w-5 h-5 text-blue-500" />
-              {t("notifications.dailyLeaderboard.title")}
-            </CardTitle>
-            <CardDescription>{t("notifications.dailyLeaderboard.description")}</CardDescription>
-          </CardHeader>
-          <CardContent className="space-y-4">
-            <div className="flex items-center justify-between">
-              <Label htmlFor="dailyLeaderboardEnabled">
-                {t("notifications.dailyLeaderboard.enable")}
-              </Label>
-              <Switch
-                id="dailyLeaderboardEnabled"
-                checked={dailyLeaderboardEnabled}
-                disabled={!enabled}
-                onCheckedChange={(checked) => setValue("dailyLeaderboardEnabled", checked)}
-              />
-            </div>
-
-            {dailyLeaderboardEnabled && (
-              <div className="space-y-4 pt-4">
-                <Separator />
-                <div className="space-y-2">
-                  <Label htmlFor="dailyLeaderboardWebhook">
-                    {t("notifications.dailyLeaderboard.webhook")}
-                  </Label>
-                  <Input
-                    id="dailyLeaderboardWebhook"
-                    {...register("dailyLeaderboardWebhook")}
-                    placeholder={t("notifications.dailyLeaderboard.webhookPlaceholder")}
-                    disabled={!enabled}
-                  />
-                  {errors.dailyLeaderboardWebhook && (
-                    <p className="text-sm text-red-500">{errors.dailyLeaderboardWebhook.message}</p>
-                  )}
+              {dailyLeaderboardEnabled && (
+                <div className="space-y-4 pt-4">
+                  <Separator />
+                  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+                    <div className="space-y-2">
+                      <Label htmlFor="dailyLeaderboardWebhook">
+                        {t("notifications.dailyLeaderboard.webhook")}
+                      </Label>
+                      <Input
+                        id="dailyLeaderboardWebhook"
+                        {...register("dailyLeaderboardWebhook")}
+                        placeholder={t("notifications.dailyLeaderboard.webhookPlaceholder")}
+                        disabled={!enabled}
+                      />
+                      {errors.dailyLeaderboardWebhook && (
+                        <p className="text-sm text-red-500">
+                          {errors.dailyLeaderboardWebhook.message}
+                        </p>
+                      )}
+                    </div>
+
+                    <div className="space-y-2">
+                      <Label htmlFor="dailyLeaderboardTime">
+                        {t("notifications.dailyLeaderboard.time")}
+                      </Label>
+                      <Input
+                        id="dailyLeaderboardTime"
+                        type="time"
+                        {...register("dailyLeaderboardTime")}
+                        disabled={!enabled}
+                      />
+                    </div>
+
+                    <div className="space-y-2">
+                      <Label htmlFor="dailyLeaderboardTopN">
+                        {t("notifications.dailyLeaderboard.topN")}
+                      </Label>
+                      <Input
+                        id="dailyLeaderboardTopN"
+                        type="number"
+                        min={1}
+                        max={20}
+                        {...register("dailyLeaderboardTopN", { valueAsNumber: true })}
+                        disabled={!enabled}
+                      />
+                    </div>
+                  </div>
+
+                  <Button
+                    type="button"
+                    variant="outline"
+                    size="sm"
+                    disabled={!enabled || testingWebhook === "dailyLeaderboard"}
+                    onClick={() =>
+                      handleTestWebhook(watch("dailyLeaderboardWebhook") || "", "dailyLeaderboard")
+                    }
+                  >
+                    {testingWebhook === "dailyLeaderboard" ? (
+                      <>
+                        <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                        {t("common.testing")}
+                      </>
+                    ) : (
+                      <>
+                        <TestTube className="w-4 h-4 mr-2" />
+                        {t("notifications.dailyLeaderboard.test")}
+                      </>
+                    )}
+                  </Button>
                 </div>
+              )}
+            </CardContent>
+          </Card>
+
+          {/* 成本预警配置 */}
+          <Card>
+            <CardHeader>
+              <CardTitle className="flex items-center gap-2">
+                <TrendingUp className="w-5 h-5 text-orange-500" />
+                {t("notifications.costAlert.title")}
+              </CardTitle>
+              <CardDescription>{t("notifications.costAlert.description")}</CardDescription>
+            </CardHeader>
+            <CardContent className="space-y-4">
+              <div className="flex items-center justify-between">
+                <Label htmlFor="costAlertEnabled">{t("notifications.costAlert.enable")}</Label>
+                <Switch
+                  id="costAlertEnabled"
+                  checked={costAlertEnabled}
+                  disabled={!enabled}
+                  onCheckedChange={(checked) => setValue("costAlertEnabled", checked)}
+                />
+              </div>
 
-                <div className="grid grid-cols-2 gap-4">
+              {costAlertEnabled && (
+                <div className="space-y-4 pt-4">
+                  <Separator />
                   <div className="space-y-2">
-                    <Label htmlFor="dailyLeaderboardTime">
-                      {t("notifications.dailyLeaderboard.time")}
-                    </Label>
+                    <Label htmlFor="costAlertWebhook">{t("notifications.costAlert.webhook")}</Label>
                     <Input
-                      id="dailyLeaderboardTime"
-                      {...register("dailyLeaderboardTime")}
-                      placeholder={t("notifications.dailyLeaderboard.timePlaceholder")}
+                      id="costAlertWebhook"
+                      {...register("costAlertWebhook")}
+                      placeholder={t("notifications.costAlert.webhookPlaceholder")}
                       disabled={!enabled}
                     />
-                    {errors.dailyLeaderboardTime && (
-                      <p className="text-sm text-red-500">{errors.dailyLeaderboardTime.message}</p>
+                    {errors.costAlertWebhook && (
+                      <p className="text-sm text-red-500">{errors.costAlertWebhook.message}</p>
                     )}
                   </div>
 
                   <div className="space-y-2">
-                    <Label htmlFor="dailyLeaderboardTopN">
-                      {t("notifications.dailyLeaderboard.topN")}
+                    <Label>{t("notifications.costAlert.threshold")}</Label>
+                    <div className="flex items-center gap-4">
+                      <Slider
+                        value={[costAlertThreshold]}
+                        min={0.5}
+                        max={1}
+                        step={0.05}
+                        onValueChange={([value]) => setValue("costAlertThreshold", value)}
+                        disabled={!enabled}
+                        className="flex-1"
+                      />
+                      <span className="w-12 text-right text-sm font-medium">
+                        {(costAlertThreshold * 100).toFixed(0)}%
+                      </span>
+                    </div>
+                  </div>
+
+                  <div className="space-y-2">
+                    <Label htmlFor="costAlertCheckInterval">
+                      {t("notifications.costAlert.checkInterval")}
                     </Label>
                     <Input
-                      id="dailyLeaderboardTopN"
+                      id="costAlertCheckInterval"
                       type="number"
-                      {...register("dailyLeaderboardTopN", { valueAsNumber: true })}
-                      min={1}
-                      max={20}
+                      min={10}
+                      max={1440}
+                      {...register("costAlertCheckInterval", { valueAsNumber: true })}
                       disabled={!enabled}
                     />
-                    {errors.dailyLeaderboardTopN && (
-                      <p className="text-sm text-red-500">{errors.dailyLeaderboardTopN.message}</p>
-                    )}
                   </div>
-                </div>
 
-                <Button
-                  type="button"
-                  variant="outline"
-                  size="sm"
-                  disabled={!enabled || testingWebhook === "leaderboard"}
-                  onClick={() =>
-                    handleTestWebhook(watch("dailyLeaderboardWebhook") || "", "leaderboard")
-                  }
-                >
-                  {testingWebhook === "leaderboard" ? (
-                    <>
-                      <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                      {t("common.testing")}
-                    </>
-                  ) : (
-                    <>
-                      <TestTube className="w-4 h-4 mr-2" />
-                      {t("notifications.dailyLeaderboard.test")}
-                    </>
-                  )}
-                </Button>
-              </div>
-            )}
-          </CardContent>
-        </Card>
+                  <Button
+                    type="button"
+                    variant="outline"
+                    size="sm"
+                    disabled={!enabled || testingWebhook === "costAlert"}
+                    onClick={() => handleTestWebhook(watch("costAlertWebhook") || "", "costAlert")}
+                  >
+                    {testingWebhook === "costAlert" ? (
+                      <>
+                        <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                        {t("common.testing")}
+                      </>
+                    ) : (
+                      <>
+                        <TestTube className="w-4 h-4 mr-2" />
+                        {t("notifications.costAlert.test")}
+                      </>
+                    )}
+                  </Button>
+                </div>
+              )}
+            </CardContent>
+          </Card>
+
+          <div className="flex justify-end">
+            <Button type="submit" disabled={isSubmitting}>
+              {isSubmitting ? t("notifications.form.saving") : t("notifications.form.save")}
+            </Button>
+          </div>
+        </form>
+      )}
+    </div>
+  );
+}
 
-        {/* 成本预警配置 */}
-        <Card>
+function NotificationsSkeleton({ label }: { label: string }) {
+  return (
+    <div className="space-y-6" aria-busy="true">
+      {Array.from({ length: 3 }).map((_, index) => (
+        <Card key={index}>
           <CardHeader>
-            <CardTitle className="flex items-center gap-2">
-              <AlertTriangle className="w-5 h-5 text-yellow-500" />
-              {t("notifications.costAlert.title")}
-            </CardTitle>
-            <CardDescription>{t("notifications.costAlert.description")}</CardDescription>
+            <Skeleton className="h-5 w-48" />
+            <Skeleton className="h-4 w-64" />
           </CardHeader>
-          <CardContent className="space-y-4">
-            <div className="flex items-center justify-between">
-              <Label htmlFor="costAlertEnabled">{t("notifications.costAlert.enable")}</Label>
-              <Switch
-                id="costAlertEnabled"
-                checked={costAlertEnabled}
-                disabled={!enabled}
-                onCheckedChange={(checked) => setValue("costAlertEnabled", checked)}
-              />
-            </div>
-
-            {costAlertEnabled && (
-              <div className="space-y-4 pt-4">
-                <Separator />
-                <div className="space-y-2">
-                  <Label htmlFor="costAlertWebhook">{t("notifications.costAlert.webhook")}</Label>
-                  <Input
-                    id="costAlertWebhook"
-                    {...register("costAlertWebhook")}
-                    placeholder={t("notifications.costAlert.webhookPlaceholder")}
-                    disabled={!enabled}
-                  />
-                  {errors.costAlertWebhook && (
-                    <p className="text-sm text-red-500">{errors.costAlertWebhook.message}</p>
-                  )}
-                </div>
-
-                <div className="space-y-2">
-                  <Label htmlFor="costAlertThreshold">
-                    {t("notifications.costAlert.thresholdLabel", {
-                      percent: ((costAlertThreshold || 0.8) * 100).toFixed(0),
-                    })}
-                  </Label>
-                  <Slider
-                    id="costAlertThreshold"
-                    min={0.5}
-                    max={1.0}
-                    step={0.05}
-                    value={[costAlertThreshold || 0.8]}
-                    onValueChange={([value]) => setValue("costAlertThreshold", value)}
-                    disabled={!enabled}
-                    className="w-full"
-                  />
-                  <p className="text-sm text-muted-foreground">
-                    {t("notifications.costAlert.thresholdHelp", {
-                      percent: ((costAlertThreshold || 0.8) * 100).toFixed(0),
-                    })}
-                  </p>
-                </div>
-
-                <div className="space-y-2">
-                  <Label htmlFor="costAlertCheckInterval">
-                    {t("notifications.costAlert.interval")}
-                  </Label>
-                  <Input
-                    id="costAlertCheckInterval"
-                    type="number"
-                    {...register("costAlertCheckInterval", { valueAsNumber: true })}
-                    min={10}
-                    max={1440}
-                    disabled={!enabled}
-                  />
-                  {errors.costAlertCheckInterval && (
-                    <p className="text-sm text-red-500">{errors.costAlertCheckInterval.message}</p>
-                  )}
-                </div>
-
-                <Button
-                  type="button"
-                  variant="outline"
-                  size="sm"
-                  disabled={!enabled || testingWebhook === "cost"}
-                  onClick={() => handleTestWebhook(watch("costAlertWebhook") || "", "cost")}
-                >
-                  {testingWebhook === "cost" ? (
-                    <>
-                      <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                      {t("common.testing")}
-                    </>
-                  ) : (
-                    <>
-                      <TestTube className="w-4 h-4 mr-2" />
-                      {t("notifications.costAlert.test")}
-                    </>
-                  )}
-                </Button>
-              </div>
-            )}
+          <CardContent className="space-y-3">
+            <Skeleton className="h-4 w-full" />
+            <Skeleton className="h-9 w-full" />
+            <Skeleton className="h-9 w-40" />
           </CardContent>
         </Card>
-
-        {/* 保存按钮 */}
-        <div className="flex justify-end">
-          <Button type="submit" disabled={isSubmitting}>
-            {isSubmitting ? (
-              <>
-                <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                {t("notifications.form.saving")}
-              </>
-            ) : (
-              t("notifications.form.save")
-            )}
-          </Button>
-        </div>
-      </form>
+      ))}
+      <div className="flex items-center gap-2 text-xs text-muted-foreground">
+        <Loader2 className="h-3 w-3 animate-spin" />
+        <span>{label}</span>
+      </div>
     </div>
   );
 }

+ 23 - 0
src/app/[locale]/settings/prices/_components/prices-skeleton.tsx

@@ -0,0 +1,23 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function PricesSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <div className="flex items-center justify-between">
+          <div className="space-y-2">
+            <Skeleton className="h-5 w-40" />
+            <Skeleton className="h-4 w-64" />
+          </div>
+          <div className="flex gap-2">
+            <Skeleton className="h-9 w-24" />
+            <Skeleton className="h-9 w-24" />
+          </div>
+        </div>
+        <TableSkeleton rows={8} columns={5} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}

+ 32 - 21
src/app/[locale]/settings/prices/page.tsx

@@ -1,8 +1,10 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { getModelPrices, getModelPricesPaginated } from "@/actions/model-prices";
 import { Section } from "@/components/section";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { PriceList } from "./_components/price-list";
+import { PricesSkeleton } from "./_components/prices-skeleton";
 import { SyncLiteLLMButton } from "./_components/sync-litellm-button";
 import { UploadPriceDialog } from "./_components/upload-price-dialog";
 
@@ -20,6 +22,19 @@ interface SettingsPricesPageProps {
 
 export default async function SettingsPricesPage({ searchParams }: SettingsPricesPageProps) {
   const t = await getTranslations("settings");
+
+  return (
+    <>
+      <SettingsPageHeader title={t("prices.title")} description={t("prices.description")} />
+      <Suspense fallback={<PricesSkeleton />}>
+        <SettingsPricesContent searchParams={searchParams} />
+      </Suspense>
+    </>
+  );
+}
+
+async function SettingsPricesContent({ searchParams }: SettingsPricesPageProps) {
+  const t = await getTranslations("settings");
   const params = await searchParams;
 
   // 解析分页参数
@@ -53,26 +68,22 @@ export default async function SettingsPricesPage({ searchParams }: SettingsPrice
   const isEmpty = initialTotal === 0;
 
   return (
-    <>
-      <SettingsPageHeader title={t("prices.title")} description={t("prices.description")} />
-
-      <Section
-        title={t("prices.section.title")}
-        description={t("prices.section.description")}
-        actions={
-          <div className="flex gap-2">
-            <SyncLiteLLMButton />
-            <UploadPriceDialog defaultOpen={isRequired && isEmpty} isRequired={isRequired} />
-          </div>
-        }
-      >
-        <PriceList
-          initialPrices={initialPrices}
-          initialTotal={initialTotal}
-          initialPage={initialPage}
-          initialPageSize={initialPageSize}
-        />
-      </Section>
-    </>
+    <Section
+      title={t("prices.section.title")}
+      description={t("prices.section.description")}
+      actions={
+        <div className="flex gap-2">
+          <SyncLiteLLMButton />
+          <UploadPriceDialog defaultOpen={isRequired && isEmpty} isRequired={isRequired} />
+        </div>
+      }
+    >
+      <PriceList
+        initialPrices={initialPrices}
+        initialTotal={initialTotal}
+        initialPage={initialPage}
+        initialPageSize={initialPageSize}
+      />
+    </Section>
   );
 }

+ 4 - 0
src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx

@@ -1,4 +1,5 @@
 "use client";
+import { useQueryClient } from "@tanstack/react-query";
 import { ServerCog } from "lucide-react";
 import { useRouter } from "next/navigation";
 import { useState } from "react";
@@ -13,6 +14,7 @@ interface AddProviderDialogProps {
 
 export function AddProviderDialog({ enableMultiProviderTypes }: AddProviderDialogProps) {
   const router = useRouter();
+  const queryClient = useQueryClient();
   const [open, setOpen] = useState(false);
   return (
     <Dialog open={open} onOpenChange={setOpen}>
@@ -28,6 +30,8 @@ export function AddProviderDialog({ enableMultiProviderTypes }: AddProviderDialo
             enableMultiProviderTypes={enableMultiProviderTypes}
             onSuccess={() => {
               setOpen(false);
+              queryClient.invalidateQueries({ queryKey: ["providers"] });
+              queryClient.invalidateQueries({ queryKey: ["providers-health"] });
               // 刷新页面数据以显示新添加的服务商
               router.refresh();
             }}

+ 101 - 0
src/app/[locale]/settings/providers/_components/provider-manager-loader.tsx

@@ -0,0 +1,101 @@
+"use client";
+
+import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
+import type { ReactNode } from "react";
+import { getProviders, getProvidersHealthStatus } from "@/actions/providers";
+import type { CurrencyCode } from "@/lib/utils/currency";
+import type { ProviderDisplay } from "@/types/provider";
+import type { User } from "@/types/user";
+import { ProviderManager } from "./provider-manager";
+
+type ProviderHealthStatus = Record<
+  number,
+  {
+    circuitState: "closed" | "open" | "half-open";
+    failureCount: number;
+    lastFailureTime: number | null;
+    circuitOpenUntil: number | null;
+    recoveryMinutes: number | null;
+  }
+>;
+
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      refetchOnWindowFocus: false,
+      staleTime: 30000,
+    },
+  },
+});
+
+async function fetchSystemSettings(): Promise<{ currencyDisplay: CurrencyCode }> {
+  const response = await fetch("/api/system-settings");
+  if (!response.ok) {
+    throw new Error("FETCH_SETTINGS_FAILED");
+  }
+  return response.json() as Promise<{ currencyDisplay: CurrencyCode }>;
+}
+
+interface ProviderManagerLoaderProps {
+  currentUser?: User;
+  enableMultiProviderTypes: boolean;
+  addDialogSlot?: ReactNode;
+}
+
+function ProviderManagerLoaderContent({
+  currentUser,
+  enableMultiProviderTypes,
+  addDialogSlot,
+}: ProviderManagerLoaderProps) {
+  const {
+    data: providers = [],
+    isLoading: isProvidersLoading,
+    isFetching: isProvidersFetching,
+  } = useQuery<ProviderDisplay[]>({
+    queryKey: ["providers"],
+    queryFn: getProviders,
+  });
+
+  const {
+    data: healthStatus = {} as ProviderHealthStatus,
+    isLoading: isHealthLoading,
+    isFetching: isHealthFetching,
+  } = useQuery<ProviderHealthStatus>({
+    queryKey: ["providers-health"],
+    queryFn: getProvidersHealthStatus,
+  });
+
+  const {
+    data: systemSettings,
+    isLoading: isSettingsLoading,
+    isFetching: isSettingsFetching,
+  } = useQuery<{ currencyDisplay: CurrencyCode }>({
+    queryKey: ["system-settings"],
+    queryFn: fetchSystemSettings,
+  });
+
+  const loading = isProvidersLoading || isHealthLoading || isSettingsLoading;
+  const refreshing = !loading && (isProvidersFetching || isHealthFetching || isSettingsFetching);
+  const currencyCode = systemSettings?.currencyDisplay ?? "USD";
+
+  return (
+    <ProviderManager
+      providers={providers}
+      currentUser={currentUser}
+      healthStatus={healthStatus}
+      currencyCode={currencyCode}
+      enableMultiProviderTypes={enableMultiProviderTypes}
+      loading={loading}
+      refreshing={refreshing}
+      addDialogSlot={addDialogSlot}
+    />
+  );
+}
+
+export function ProviderManagerLoader(props: ProviderManagerLoaderProps) {
+  return (
+    <QueryClientProvider client={queryClient}>
+      <ProviderManagerLoaderContent {...props} />
+    </QueryClientProvider>
+  );
+}

+ 70 - 18
src/app/[locale]/settings/providers/_components/provider-manager.tsx

@@ -1,8 +1,9 @@
 "use client";
-import { Search, X } from "lucide-react";
+import { Loader2, Search, X } from "lucide-react";
 import { useTranslations } from "next-intl";
-import { useMemo, useState } from "react";
+import { type ReactNode, useMemo, useState } from "react";
 import { Input } from "@/components/ui/input";
+import { Skeleton } from "@/components/ui/skeleton";
 import { useDebounce } from "@/lib/hooks/use-debounce";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import type { ProviderDisplay, ProviderType } from "@/types/provider";
@@ -26,6 +27,9 @@ interface ProviderManagerProps {
   >;
   currencyCode?: CurrencyCode;
   enableMultiProviderTypes: boolean;
+  loading?: boolean;
+  refreshing?: boolean;
+  addDialogSlot?: ReactNode;
 }
 
 export function ProviderManager({
@@ -34,8 +38,12 @@ export function ProviderManager({
   healthStatus,
   currencyCode = "USD",
   enableMultiProviderTypes,
+  loading = false,
+  refreshing = false,
+  addDialogSlot,
 }: ProviderManagerProps) {
   const t = useTranslations("settings.providers.search");
+  const tCommon = useTranslations("settings.common");
   const [typeFilter, setTypeFilter] = useState<ProviderType | "all">("all");
   const [sortBy, setSortBy] = useState<SortKey>("priority");
   const [searchTerm, setSearchTerm] = useState("");
@@ -91,11 +99,12 @@ export function ProviderManager({
 
   return (
     <div className="space-y-4">
+      {addDialogSlot ? <div className="flex justify-end">{addDialogSlot}</div> : null}
       {/* 筛选条件 */}
       <div className="flex flex-col gap-3">
         <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
-          <ProviderTypeFilter value={typeFilter} onChange={setTypeFilter} />
-          <ProviderSortDropdown value={sortBy} onChange={setSortBy} />
+          <ProviderTypeFilter value={typeFilter} onChange={setTypeFilter} disabled={loading} />
+          <ProviderSortDropdown value={sortBy} onChange={setSortBy} disabled={loading} />
           <div className="relative flex-1">
             <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
             <Input
@@ -104,12 +113,14 @@ export function ProviderManager({
               value={searchTerm}
               onChange={(e) => setSearchTerm(e.target.value)}
               className="pl-9 pr-9"
+              disabled={loading}
             />
             {searchTerm && (
               <button
                 onClick={() => setSearchTerm("")}
                 className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
                 aria-label={t("clear")}
+                disabled={loading}
               >
                 <X className="h-4 w-4" />
               </button>
@@ -117,30 +128,71 @@ export function ProviderManager({
           </div>
         </div>
         {/* 搜索结果提示 */}
-        {debouncedSearchTerm && (
+        {debouncedSearchTerm ? (
           <p className="text-sm text-muted-foreground">
-            {filteredProviders.length > 0
-              ? t("found", { count: filteredProviders.length })
-              : t("notFound")}
+            {loading
+              ? tCommon("loading")
+              : filteredProviders.length > 0
+                ? t("found", { count: filteredProviders.length })
+                : t("notFound")}
           </p>
-        )}
-        {!debouncedSearchTerm && (
+        ) : (
           <div className="text-sm text-muted-foreground">
-            {t("showing", { filtered: filteredProviders.length, total: providers.length })}
+            {loading
+              ? tCommon("loading")
+              : t("showing", { filtered: filteredProviders.length, total: providers.length })}
           </div>
         )}
       </div>
 
       {/* 供应商列表 */}
-      <ProviderList
-        providers={filteredProviders}
-        currentUser={currentUser}
-        healthStatus={healthStatus}
-        currencyCode={currencyCode}
-        enableMultiProviderTypes={enableMultiProviderTypes}
-      />
+      {loading && providers.length === 0 ? (
+        <ProviderListSkeleton label={tCommon("loading")} />
+      ) : (
+        <div className="space-y-3">
+          {refreshing ? <InlineLoading label={tCommon("loading")} /> : null}
+          <ProviderList
+            providers={filteredProviders}
+            currentUser={currentUser}
+            healthStatus={healthStatus}
+            currencyCode={currencyCode}
+            enableMultiProviderTypes={enableMultiProviderTypes}
+          />
+        </div>
+      )}
     </div>
   );
 }
 
 export type { ProviderDisplay } from "@/types/provider";
+
+function InlineLoading({ label }: { label: string }) {
+  return (
+    <div className="flex items-center gap-2 text-xs text-muted-foreground" aria-live="polite">
+      <Loader2 className="h-3 w-3 animate-spin" />
+      <span>{label}</span>
+    </div>
+  );
+}
+
+function ProviderListSkeleton({ label }: { label: string }) {
+  return (
+    <div className="space-y-3" aria-busy="true">
+      {Array.from({ length: 4 }).map((_, index) => (
+        <div key={index} className="rounded-lg border bg-card p-4 space-y-3">
+          <div className="flex items-center justify-between gap-3">
+            <Skeleton className="h-5 w-40" />
+            <Skeleton className="h-5 w-20" />
+          </div>
+          <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
+            <Skeleton className="h-4 w-full" />
+            <Skeleton className="h-4 w-full" />
+            <Skeleton className="h-4 w-full" />
+          </div>
+          <Skeleton className="h-8 w-full" />
+        </div>
+      ))}
+      <InlineLoading label={label} />
+    </div>
+  );
+}

+ 12 - 0
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx

@@ -1,4 +1,5 @@
 "use client";
+import { useQueryClient } from "@tanstack/react-query";
 import {
   AlertTriangle,
   CheckCircle,
@@ -77,6 +78,7 @@ export function ProviderRichListItem({
   onDelete: onDeleteProp,
 }: ProviderRichListItemProps) {
   const router = useRouter();
+  const queryClient = useQueryClient();
   const [openEdit, setOpenEdit] = useState(false);
   const [openClone, setOpenClone] = useState(false);
   const [showKeyDialog, setShowKeyDialog] = useState(false);
@@ -133,6 +135,8 @@ export function ProviderRichListItem({
             toast.success(tList("deleteSuccess"), {
               description: tList("deleteSuccessDesc", { name: provider.name }),
             });
+            queryClient.invalidateQueries({ queryKey: ["providers"] });
+            queryClient.invalidateQueries({ queryKey: ["providers-health"] });
             router.refresh();
           } else {
             toast.error(tList("deleteFailed"), {
@@ -202,6 +206,8 @@ export function ProviderRichListItem({
           toast.success(tList("resetCircuitSuccess"), {
             description: tList("resetCircuitSuccessDesc", { name: provider.name }),
           });
+          queryClient.invalidateQueries({ queryKey: ["providers"] });
+          queryClient.invalidateQueries({ queryKey: ["providers-health"] });
           router.refresh();
         } else {
           toast.error(tList("resetCircuitFailed"), {
@@ -229,6 +235,8 @@ export function ProviderRichListItem({
           toast.success(tList("toggleSuccess", { status }), {
             description: tList("toggleSuccessDesc", { name: provider.name }),
           });
+          queryClient.invalidateQueries({ queryKey: ["providers"] });
+          queryClient.invalidateQueries({ queryKey: ["providers-health"] });
           router.refresh();
         } else {
           toast.error(tList("toggleFailed"), {
@@ -489,6 +497,8 @@ export function ProviderRichListItem({
               provider={provider}
               onSuccess={() => {
                 setOpenEdit(false);
+                queryClient.invalidateQueries({ queryKey: ["providers"] });
+                queryClient.invalidateQueries({ queryKey: ["providers-health"] });
                 router.refresh();
               }}
               enableMultiProviderTypes={enableMultiProviderTypes}
@@ -506,6 +516,8 @@ export function ProviderRichListItem({
               cloneProvider={provider}
               onSuccess={() => {
                 setOpenClone(false);
+                queryClient.invalidateQueries({ queryKey: ["providers"] });
+                queryClient.invalidateQueries({ queryKey: ["providers-health"] });
                 router.refresh();
               }}
               enableMultiProviderTypes={enableMultiProviderTypes}

+ 12 - 3
src/app/[locale]/settings/providers/_components/provider-sort-dropdown.tsx

@@ -15,9 +15,14 @@ export type SortKey = "name" | "priority" | "weight" | "createdAt";
 interface ProviderSortDropdownProps {
   value: SortKey;
   onChange: (value: SortKey) => void;
+  disabled?: boolean;
 }
 
-export function ProviderSortDropdown({ value, onChange }: ProviderSortDropdownProps) {
+export function ProviderSortDropdown({
+  value,
+  onChange,
+  disabled = false,
+}: ProviderSortDropdownProps) {
   const t = useTranslations("settings.providers.sort");
   const selectedValue = value ?? "priority";
 
@@ -31,8 +36,12 @@ export function ProviderSortDropdown({ value, onChange }: ProviderSortDropdownPr
   return (
     <div className="flex items-center gap-2">
       <ArrowUpDown className="h-4 w-4 text-muted-foreground" />
-      <Select value={selectedValue} onValueChange={(nextValue) => onChange(nextValue as SortKey)}>
-        <SelectTrigger className="w-[200px]">
+      <Select
+        value={selectedValue}
+        onValueChange={(nextValue) => onChange(nextValue as SortKey)}
+        disabled={disabled}
+      >
+        <SelectTrigger className="w-[200px]" disabled={disabled}>
           <SelectValue placeholder={t("placeholder")} />
         </SelectTrigger>
         <SelectContent>

+ 4 - 3
src/app/[locale]/settings/providers/_components/provider-type-filter.tsx

@@ -18,17 +18,18 @@ import type { ProviderType } from "@/types/provider";
 interface ProviderTypeFilterProps {
   value: ProviderType | "all";
   onChange: (value: ProviderType | "all") => void;
+  disabled?: boolean;
 }
 
-export function ProviderTypeFilter({ value, onChange }: ProviderTypeFilterProps) {
+export function ProviderTypeFilter({ value, onChange, disabled = false }: ProviderTypeFilterProps) {
   const tTypes = useTranslations("settings.providers.types");
   const tForm = useTranslations("settings.providers.form");
 
   return (
     <div className="flex items-center gap-2">
       <Filter className="h-4 w-4 text-muted-foreground" />
-      <Select value={value} onValueChange={onChange}>
-        <SelectTrigger className="w-[200px]">
+      <Select value={value} onValueChange={onChange} disabled={disabled}>
+        <SelectTrigger className="w-[200px]" disabled={disabled}>
           <SelectValue placeholder={tForm("filterByType")} />
         </SelectTrigger>
         <SelectContent>

+ 4 - 14
src/app/[locale]/settings/providers/page.tsx

@@ -1,27 +1,20 @@
 import { BarChart3 } from "lucide-react";
 import { getTranslations } from "next-intl/server";
-import { getProviders, getProvidersHealthStatus } from "@/actions/providers";
 import { Section } from "@/components/section";
 import { Button } from "@/components/ui/button";
 import { Link } from "@/i18n/routing";
 import { getSession } from "@/lib/auth";
 import { getEnvConfig } from "@/lib/config/env.schema";
-import { getSystemSettings } from "@/repository/system-config";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { AddProviderDialog } from "./_components/add-provider-dialog";
-import { ProviderManager } from "./_components/provider-manager";
+import { ProviderManagerLoader } from "./_components/provider-manager-loader";
 import { SchedulingRulesDialog } from "./_components/scheduling-rules-dialog";
 
 export const dynamic = "force-dynamic";
 
 export default async function SettingsProvidersPage() {
   const t = await getTranslations("settings");
-  const [providers, session, healthStatus, systemSettings] = await Promise.all([
-    getProviders(),
-    getSession(),
-    getProvidersHealthStatus(),
-    getSystemSettings(),
-  ]);
+  const session = await getSession();
 
   // 读取多供应商类型支持配置
   const enableMultiProviderTypes = getEnvConfig().ENABLE_MULTI_PROVIDER_TYPES;
@@ -42,16 +35,13 @@ export default async function SettingsProvidersPage() {
               </Link>
             </Button>
             <SchedulingRulesDialog />
-            <AddProviderDialog enableMultiProviderTypes={enableMultiProviderTypes} />
           </>
         }
       >
-        <ProviderManager
-          providers={providers}
+        <ProviderManagerLoader
           currentUser={session?.user}
-          healthStatus={healthStatus}
-          currencyCode={systemSettings.currencyDisplay}
           enableMultiProviderTypes={enableMultiProviderTypes}
+          addDialogSlot={<AddProviderDialog enableMultiProviderTypes={enableMultiProviderTypes} />}
         />
       </Section>
     </>

+ 26 - 0
src/app/[locale]/settings/request-filters/_components/request-filters-skeleton.tsx

@@ -0,0 +1,26 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function RequestFiltersSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <Skeleton className="h-5 w-40" />
+        <Skeleton className="h-4 w-56" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-4">
+        <TableSkeleton rows={6} columns={4} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}
+
+export function RequestFiltersTableSkeleton() {
+  return (
+    <div className="rounded-xl border bg-card p-5 space-y-4" aria-busy="true">
+      <TableSkeleton rows={6} columns={4} />
+      <LoadingState />
+    </div>
+  );
+}

+ 11 - 3
src/app/[locale]/settings/request-filters/page.tsx

@@ -1,22 +1,30 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { listRequestFilters } from "@/actions/request-filters";
 import { Section } from "@/components/section";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { FilterTable } from "./_components/filter-table";
+import { RequestFiltersTableSkeleton } from "./_components/request-filters-skeleton";
 
 export const dynamic = "force-dynamic";
 
 export default async function RequestFiltersPage() {
   const t = await getTranslations("settings.requestFilters");
-  const filters = await listRequestFilters();
 
   return (
     <div className="space-y-6">
       <SettingsPageHeader title={t("title")} description={t("description")} />
-
       <Section title={t("title")} description={t("description")}>
-        <FilterTable filters={filters} />
+        <Suspense fallback={<RequestFiltersTableSkeleton />}>
+          <RequestFiltersContent />
+        </Suspense>
       </Section>
     </div>
   );
 }
+
+async function RequestFiltersContent() {
+  const filters = await listRequestFilters();
+
+  return <FilterTable filters={filters} />;
+}

+ 33 - 0
src/app/[locale]/settings/sensitive-words/_components/sensitive-words-skeleton.tsx

@@ -0,0 +1,33 @@
+import { LoadingState, TableSkeleton } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function SensitiveWordsSkeleton() {
+  return (
+    <div className="space-y-6">
+      <div className="rounded-xl border bg-card p-5 space-y-3">
+        <Skeleton className="h-5 w-40" />
+        <Skeleton className="h-4 w-56" />
+      </div>
+      <div className="rounded-xl border bg-card p-5 space-y-4">
+        <div className="flex items-center justify-between">
+          <Skeleton className="h-5 w-36" />
+          <div className="flex gap-2">
+            <Skeleton className="h-9 w-24" />
+            <Skeleton className="h-9 w-24" />
+          </div>
+        </div>
+        <TableSkeleton rows={6} columns={3} />
+        <LoadingState />
+      </div>
+    </div>
+  );
+}
+
+export function SensitiveWordsTableSkeleton() {
+  return (
+    <div className="rounded-xl border bg-card p-5 space-y-4" aria-busy="true">
+      <TableSkeleton rows={6} columns={3} />
+      <LoadingState />
+    </div>
+  );
+}

+ 19 - 4
src/app/[locale]/settings/sensitive-words/page.tsx

@@ -1,16 +1,18 @@
 import { getTranslations } from "next-intl/server";
+import { Suspense } from "react";
 import { getCacheStats, listSensitiveWords } from "@/actions/sensitive-words";
 import { Section } from "@/components/section";
+import { Skeleton } from "@/components/ui/skeleton";
 import { SettingsPageHeader } from "../_components/settings-page-header";
 import { AddWordDialog } from "./_components/add-word-dialog";
 import { RefreshCacheButton } from "./_components/refresh-cache-button";
+import { SensitiveWordsTableSkeleton } from "./_components/sensitive-words-skeleton";
 import { WordListTable } from "./_components/word-list-table";
 
 export const dynamic = "force-dynamic";
 
 export default async function SensitiveWordsPage() {
   const t = await getTranslations("settings");
-  const [words, cacheStats] = await Promise.all([listSensitiveWords(), getCacheStats()]);
 
   return (
     <>
@@ -18,19 +20,32 @@ export default async function SensitiveWordsPage() {
         title={t("sensitiveWords.title")}
         description={t("sensitiveWords.description")}
       />
-
       <Section
         title={t("sensitiveWords.section.title")}
         description={t("sensitiveWords.section.description")}
         actions={
           <div className="flex gap-2">
-            <RefreshCacheButton stats={cacheStats} />
+            <Suspense fallback={<Skeleton className="h-9 w-24" />}>
+              <SensitiveWordsRefreshAction />
+            </Suspense>
             <AddWordDialog />
           </div>
         }
       >
-        <WordListTable words={words} />
+        <Suspense fallback={<SensitiveWordsTableSkeleton />}>
+          <SensitiveWordsTableContent />
+        </Suspense>
       </Section>
     </>
   );
 }
+
+async function SensitiveWordsRefreshAction() {
+  const cacheStats = await getCacheStats();
+  return <RefreshCacheButton stats={cacheStats} />;
+}
+
+async function SensitiveWordsTableContent() {
+  const words = await listSensitiveWords();
+  return <WordListTable words={words} />;
+}

+ 23 - 0
src/app/[locale]/usage-doc/loading.tsx

@@ -0,0 +1,23 @@
+import { ListSkeleton, LoadingState } from "@/components/loading/page-skeletons";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function UsageDocLoading() {
+  return (
+    <div className="space-y-6">
+      <Skeleton className="h-8 w-48" />
+      <div className="grid gap-6 lg:grid-cols-[220px_1fr]">
+        <aside className="space-y-3">
+          <Skeleton className="h-5 w-24" />
+          <ListSkeleton rows={6} />
+        </aside>
+        <div className="space-y-4">
+          <Skeleton className="h-6 w-56" />
+          <Skeleton className="h-4 w-72" />
+          <Skeleton className="h-32 w-full" />
+          <Skeleton className="h-32 w-full" />
+          <LoadingState />
+        </div>
+      </div>
+    </div>
+  );
+}

+ 14 - 11
src/app/v1/_lib/proxy/forwarder.ts

@@ -1340,17 +1340,20 @@ export class ProxyForwarder {
 
     (init as Record<string, unknown>).verbose = true;
 
-    // ⭐ 检测是否为 Gemini 供应商(需要特殊处理以绕过 undici 自动解压)
-    const isGeminiProvider =
-      provider.providerType === "gemini" || provider.providerType === "gemini-cli";
+    // ⭐ 始终使用容错流处理以减少 "TypeError: terminated" 错误
+    // 背景:undici fetch 的自动解压在流被提前终止时会抛出 "TypeError: terminated"
+    // 这个问题不仅影响 Gemini,也影响 Codex 和其他所有供应商
+    // 使用 fetchWithoutAutoDecode 绕过 undici 的自动解压,手动处理 gzip
+    // 并通过 nodeStreamToWebStreamSafe 实现容错流转换(捕获错误并优雅关闭)
+    const useErrorTolerantFetch = true;
 
     let response: Response;
     const fetchStartTime = Date.now();
     try {
-      // ⭐ Gemini 使用 undici.request 绕过 fetch 的自动解压
+      // ⭐ 所有供应商使用 undici.request 绕过 fetch 的自动解压
       // 原因:undici fetch 无法关闭自动解压,上游可能无视 accept-encoding: identity 返回 gzip
       // 当 gzip 流被提前终止时(如连接关闭),undici Gunzip 会抛出 "TypeError: terminated"
-      response = isGeminiProvider
+      response = useErrorTolerantFetch
         ? await ProxyForwarder.fetchWithoutAutoDecode(proxyUrl, init, provider.id, provider.name)
         : await fetch(proxyUrl, init);
       // ⭐ fetch 成功:收到 HTTP 响应头,保留响应超时继续监控
@@ -1509,7 +1512,7 @@ export class ProxyForwarder {
               errorType: "Http2Error",
               errorName: err.name,
               errorMessage: err.message || err.name || "HTTP/2 protocol error",
-              errorCode: err.code || "HTTP2_FAILED",
+              errorCode: err.code,
               errorStack: err.stack?.split("\n").slice(0, 3).join("\n"),
             },
             // W-011: 添加 request 字段以保持与其他错误处理一致
@@ -1531,7 +1534,7 @@ export class ProxyForwarder {
 
         try {
           // 使用 HTTP/1.1 重试
-          response = isGeminiProvider
+          response = useErrorTolerantFetch
             ? await ProxyForwarder.fetchWithoutAutoDecode(
                 proxyUrl,
                 http1FallbackInit,
@@ -1631,7 +1634,7 @@ export class ProxyForwarder {
             errorSyscall: err.syscall, // ⭐ 如 'getaddrinfo'(DNS查询)、'connect'(TCP连接)
             errorErrno: err.errno,
             errorCause: err.cause,
-
+            // ⭐ 增强诊断:undici 参数验证错误的具体说明
             errorCauseMessage: (err.cause as Error | undefined)?.message,
             errorCauseStack: (err.cause as Error | undefined)?.stack
               ?.split("\n")
@@ -1894,7 +1897,7 @@ export class ProxyForwarder {
       providerName,
       url: new URL(url).origin, // 只记录域名,隐藏路径和参数
       method: init.method,
-      reason: "Gemini provider requires manual gzip handling to avoid terminated error",
+      reason: "Using manual gzip handling to avoid terminated error",
     });
 
     // 将 Headers 对象转换为 Record<string, string>
@@ -1947,7 +1950,7 @@ export class ProxyForwarder {
     let bodyStream: ReadableStream<Uint8Array>;
 
     if (encoding.includes("gzip")) {
-      logger.debug("ProxyForwarder: Gemini response is gzip encoded, decompressing manually", {
+      logger.debug("ProxyForwarder: Response is gzip encoded, decompressing manually", {
         providerId,
         providerName,
         contentEncoding: encoding,
@@ -1961,7 +1964,7 @@ export class ProxyForwarder {
 
       // 捕获 Gunzip 错误但不抛出(容错处理)
       gunzip.on("error", (err) => {
-        logger.warn("ProxyForwarder: Gemini gunzip decompression error (ignored)", {
+        logger.warn("ProxyForwarder: Gunzip decompression error (ignored)", {
           providerId,
           providerName,
           error: err.message,

+ 27 - 0
src/app/v1/_lib/proxy/response-handler.ts

@@ -1835,6 +1835,31 @@ async function persistRequestFailure(options: {
   const errorMessage = formatProcessingError(error);
   const duration = Date.now() - session.startTime;
 
+  // 提取完整错误信息用于排查(限制长度防止异常大的错误信息)
+  const MAX_ERROR_STACK_LENGTH = 8192; // 8KB,足够容纳大多数堆栈信息
+  const MAX_ERROR_CAUSE_LENGTH = 4096; // 4KB,足够容纳 JSON 序列化的错误原因
+
+  let errorStack = error instanceof Error ? error.stack : undefined;
+  if (errorStack && errorStack.length > MAX_ERROR_STACK_LENGTH) {
+    errorStack = errorStack.substring(0, MAX_ERROR_STACK_LENGTH) + "\n...[truncated]";
+  }
+
+  let errorCause: string | undefined;
+  if (error instanceof Error && (error as NodeJS.ErrnoException).cause) {
+    try {
+      // 序列化错误原因链,保留所有属性
+      const cause = (error as NodeJS.ErrnoException).cause;
+      errorCause = JSON.stringify(cause, Object.getOwnPropertyNames(cause as object));
+    } catch {
+      // 如果序列化失败,使用简单字符串
+      errorCause = String((error as NodeJS.ErrnoException).cause);
+    }
+    // 截断过长的错误原因
+    if (errorCause && errorCause.length > MAX_ERROR_CAUSE_LENGTH) {
+      errorCause = errorCause.substring(0, MAX_ERROR_CAUSE_LENGTH) + "...[truncated]";
+    }
+  }
+
   try {
     // 更新请求持续时间
     await updateMessageRequestDuration(messageContext.id, duration);
@@ -1843,6 +1868,8 @@ async function persistRequestFailure(options: {
     await updateMessageRequestDetails(messageContext.id, {
       statusCode,
       errorMessage,
+      errorStack,
+      errorCause,
       providerChain: session.getProviderChain(),
       model: session.getCurrentModel() ?? undefined,
       providerId: session.provider?.id, // ⭐ 更新最终供应商ID(重试切换后)

+ 36 - 1
src/components/customs/overview-panel.tsx

@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
 import type { OverviewData } from "@/actions/overview";
 import { getOverviewData } from "@/actions/overview";
+import { Skeleton } from "@/components/ui/skeleton";
 import type { CurrencyCode } from "@/lib/utils";
 import { formatCurrency } from "@/lib/utils/currency";
 import { ActiveSessionsList } from "./active-sessions-list";
@@ -34,8 +35,9 @@ interface OverviewPanelProps {
 export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: OverviewPanelProps) {
   const router = useRouter();
   const tc = useTranslations("customs");
+  const tu = useTranslations("ui");
 
-  const { data } = useQuery<OverviewData, Error>({
+  const { data, isLoading } = useQuery<OverviewData, Error>({
     queryKey: ["overview-data"],
     queryFn: fetchOverviewData,
     refetchInterval: REFRESH_INTERVAL,
@@ -60,6 +62,39 @@ export function OverviewPanel({ currencyCode = "USD", isAdmin = false }: Overvie
     return null;
   }
 
+  if (isLoading && !data) {
+    return (
+      <div className="space-y-2">
+        <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 w-full">
+          <div className="lg:col-span-3 space-y-3">
+            <div className="grid grid-cols-2 gap-3">
+              {Array.from({ length: 4 }).map((_, index) => (
+                <div key={`metric-skeleton-${index}`} className="rounded-lg border bg-card p-4">
+                  <Skeleton className="h-4 w-20" />
+                  <Skeleton className="h-8 w-16 mt-2" />
+                </div>
+              ))}
+            </div>
+            <Skeleton className="h-8 w-full" />
+          </div>
+          <div className="lg:col-span-9">
+            <div className="rounded-lg border bg-card">
+              <div className="border-b px-4 py-3">
+                <Skeleton className="h-4 w-24" />
+              </div>
+              <div className="p-4 space-y-3">
+                {Array.from({ length: 5 }).map((_, index) => (
+                  <Skeleton key={`session-skeleton-${index}`} className="h-5 w-full" />
+                ))}
+              </div>
+            </div>
+          </div>
+        </div>
+        <div className="text-xs text-muted-foreground">{tu("common.loading")}</div>
+      </div>
+    );
+  }
+
   return (
     <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 w-full">
       {/* 左侧:指标卡片区域 */}

+ 26 - 0
src/components/loading/page-skeletons.test.tsx

@@ -0,0 +1,26 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, test } from "vitest";
+import {
+  LoadingState,
+  PageHeaderSkeleton,
+  TableSkeleton,
+} from "@/components/loading/page-skeletons";
+
+describe("page-skeletons", () => {
+  test("LoadingState renders label and aria-busy", () => {
+    const html = renderToStaticMarkup(<LoadingState label="加载中" />);
+    expect(html).toContain("加载中");
+    expect(html).toContain('aria-busy="true"');
+  });
+
+  test("PageHeaderSkeleton renders skeleton elements", () => {
+    const html = renderToStaticMarkup(<PageHeaderSkeleton />);
+    expect(html).toContain('data-slot="skeleton"');
+  });
+
+  test("TableSkeleton renders expected skeleton count", () => {
+    const html = renderToStaticMarkup(<TableSkeleton rows={2} columns={3} />);
+    const skeletonCount = (html.match(/data-slot="skeleton"/g) || []).length;
+    expect(skeletonCount).toBe(3 + 2 * 3);
+  });
+});

+ 163 - 0
src/components/loading/page-skeletons.tsx

@@ -0,0 +1,163 @@
+import type React from "react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { cn } from "@/lib/utils";
+
+interface LoadingStateProps {
+  label?: string;
+  className?: string;
+}
+
+export function LoadingState({ label = "加载中", className }: LoadingStateProps) {
+  return (
+    <div
+      role="status"
+      aria-live="polite"
+      aria-busy="true"
+      className={cn("text-xs text-muted-foreground", className)}
+    >
+      {label}
+    </div>
+  );
+}
+
+interface PageHeaderSkeletonProps {
+  titleWidth?: string;
+  descriptionWidth?: string;
+  showDescription?: boolean;
+  className?: string;
+}
+
+export function PageHeaderSkeleton({
+  titleWidth = "w-48",
+  descriptionWidth = "w-72",
+  showDescription = true,
+  className,
+}: PageHeaderSkeletonProps) {
+  return (
+    <div className={cn("space-y-2", className)}>
+      <Skeleton className={cn("h-7", titleWidth)} />
+      {showDescription ? <Skeleton className={cn("h-4", descriptionWidth)} /> : null}
+    </div>
+  );
+}
+
+interface SectionSkeletonProps {
+  titleWidth?: string;
+  descriptionWidth?: string;
+  showDescription?: boolean;
+  showActions?: boolean;
+  rows?: number;
+  className?: string;
+  body?: React.ReactNode;
+}
+
+export function SectionSkeleton({
+  titleWidth = "w-32",
+  descriptionWidth = "w-56",
+  showDescription = true,
+  showActions = false,
+  rows = 3,
+  className,
+  body,
+}: SectionSkeletonProps) {
+  return (
+    <section
+      className={cn(
+        "bg-card text-card-foreground border border-border rounded-xl shadow-sm p-5 space-y-4",
+        className
+      )}
+    >
+      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+        <div className="space-y-2">
+          <Skeleton className={cn("h-5", titleWidth)} />
+          {showDescription ? <Skeleton className={cn("h-4", descriptionWidth)} /> : null}
+        </div>
+        {showActions ? (
+          <div className="flex flex-wrap gap-2">
+            <Skeleton className="h-9 w-28" />
+            <Skeleton className="h-9 w-24" />
+          </div>
+        ) : null}
+      </div>
+      {body ? (
+        body
+      ) : (
+        <div className="space-y-3">
+          {Array.from({ length: rows }).map((_, index) => (
+            <Skeleton key={`section-row-${index}`} className="h-5 w-full" />
+          ))}
+        </div>
+      )}
+    </section>
+  );
+}
+
+interface TableSkeletonProps {
+  rows?: number;
+  columns?: number;
+  rowHeightClassName?: string;
+}
+
+export function TableSkeleton({
+  rows = 6,
+  columns = 4,
+  rowHeightClassName = "h-5",
+}: TableSkeletonProps) {
+  const columnTemplate = { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` };
+
+  return (
+    <div className="space-y-3">
+      <div className="grid gap-4" style={columnTemplate}>
+        {Array.from({ length: columns }).map((_, index) => (
+          <Skeleton key={`table-head-${index}`} className="h-4 w-full" />
+        ))}
+      </div>
+      <div className="space-y-2">
+        {Array.from({ length: rows }).map((_, rowIndex) => (
+          <div key={`table-row-${rowIndex}`} className="grid gap-4" style={columnTemplate}>
+            {Array.from({ length: columns }).map((_, colIndex) => (
+              <Skeleton
+                key={`table-cell-${rowIndex}-${colIndex}`}
+                className={cn(rowHeightClassName, "w-full")}
+              />
+            ))}
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
+
+interface CardGridSkeletonProps {
+  cards?: number;
+  className?: string;
+}
+
+export function CardGridSkeleton({ cards = 4, className }: CardGridSkeletonProps) {
+  return (
+    <div className={cn("grid grid-cols-2 gap-3", className)}>
+      {Array.from({ length: cards }).map((_, index) => (
+        <div key={`card-skeleton-${index}`} className="rounded-lg border bg-card p-4 space-y-3">
+          <Skeleton className="h-4 w-24" />
+          <Skeleton className="h-8 w-20" />
+          <Skeleton className="h-3 w-16" />
+        </div>
+      ))}
+    </div>
+  );
+}
+
+interface ListSkeletonProps {
+  rows?: number;
+  className?: string;
+}
+
+export function ListSkeleton({ rows = 5, className }: ListSkeletonProps) {
+  return (
+    <div className={cn("space-y-2", className)}>
+      {Array.from({ length: rows }).map((_, index) => (
+        <Skeleton key={`list-row-${index}`} className="h-5 w-full" />
+      ))}
+    </div>
+  );
+}

+ 2 - 0
src/drizzle/schema.ts

@@ -299,6 +299,8 @@ export const messageRequest = pgTable('message_request', {
 
   // 错误信息
   errorMessage: text('error_message'),
+  errorStack: text('error_stack'),  // 完整堆栈信息,用于排查 TypeError: terminated 等流错误
+  errorCause: text('error_cause'),  // 嵌套错误原因(JSON 格式),如 NGHTTP2_INTERNAL_ERROR
 
   // 拦截原因(用于记录被敏感词等规则拦截的请求)
   blockedBy: varchar('blocked_by', { length: 50 }),

+ 54 - 0
src/instrumentation.ts

@@ -3,7 +3,15 @@
  * 在服务器启动时自动执行数据库迁移
  */
 
+import { startCacheCleanup, stopCacheCleanup } from "@/lib/cache/session-cache";
 import { logger } from "@/lib/logger";
+import { closeRedis } from "@/lib/redis";
+
+const instrumentationState = globalThis as unknown as {
+  __CCH_CACHE_CLEANUP_STARTED__?: boolean;
+  __CCH_SHUTDOWN_HOOKS_REGISTERED__?: boolean;
+  __CCH_SHUTDOWN_IN_PROGRESS__?: boolean;
+};
 
 /**
  * 同步错误规则并初始化检测器
@@ -41,6 +49,52 @@ export async function register() {
       return;
     }
 
+    if (!instrumentationState.__CCH_CACHE_CLEANUP_STARTED__) {
+      startCacheCleanup(60);
+      instrumentationState.__CCH_CACHE_CLEANUP_STARTED__ = true;
+      logger.info("[Instrumentation] Session cache cleanup started", {
+        intervalSeconds: 60,
+      });
+    }
+
+    if (!instrumentationState.__CCH_SHUTDOWN_HOOKS_REGISTERED__) {
+      instrumentationState.__CCH_SHUTDOWN_HOOKS_REGISTERED__ = true;
+
+      const shutdownHandler = async (signal: string) => {
+        if (instrumentationState.__CCH_SHUTDOWN_IN_PROGRESS__) {
+          return;
+        }
+        instrumentationState.__CCH_SHUTDOWN_IN_PROGRESS__ = true;
+
+        logger.info(`[Instrumentation] Received ${signal}, cleaning up...`);
+
+        try {
+          stopCacheCleanup();
+          instrumentationState.__CCH_CACHE_CLEANUP_STARTED__ = false;
+        } catch (error) {
+          logger.warn("[Instrumentation] Failed to stop cache cleanup", {
+            error: error instanceof Error ? error.message : String(error),
+          });
+        }
+
+        try {
+          await closeRedis();
+        } catch (error) {
+          logger.warn("[Instrumentation] Failed to close Redis connection", {
+            error: error instanceof Error ? error.message : String(error),
+          });
+        }
+      };
+
+      process.once("SIGTERM", () => {
+        void shutdownHandler("SIGTERM");
+      });
+
+      process.once("SIGINT", () => {
+        void shutdownHandler("SIGINT");
+      });
+    }
+
     // 生产环境: 执行完整初始化(迁移 + 价格表 + 清理任务 + 通知任务)
     if (process.env.NODE_ENV === "production" && process.env.AUTO_MIGRATE !== "false") {
       const { checkDatabaseConnection, runMigrations } = await import("@/lib/migrate");

+ 22 - 1
src/lib/cache/session-cache.ts

@@ -134,6 +134,11 @@ const sessionDetailsCache = new SessionCache<{
   cacheTtlApplied: string | null;
 }>(1);
 
+// 使用 globalThis 存储 interval ID,支持热重载场景
+const cacheCleanupState = globalThis as unknown as {
+  __CCH_CACHE_CLEANUP_INTERVAL_ID__?: ReturnType<typeof setInterval> | null;
+};
+
 /**
  * 获取活跃 Sessions 的缓存
  */
@@ -201,12 +206,28 @@ export function clearAllSessionCache() {
  * 定期清理过期缓存(可选,用于内存优化)
  */
 export function startCacheCleanup(intervalSeconds: number = 60) {
-  setInterval(() => {
+  if (cacheCleanupState.__CCH_CACHE_CLEANUP_INTERVAL_ID__) {
+    return;
+  }
+
+  cacheCleanupState.__CCH_CACHE_CLEANUP_INTERVAL_ID__ = setInterval(() => {
     activeSessionsCache.cleanup();
     sessionDetailsCache.cleanup();
   }, intervalSeconds * 1000);
 }
 
+/**
+ * 停止定期清理任务
+ */
+export function stopCacheCleanup() {
+  if (!cacheCleanupState.__CCH_CACHE_CLEANUP_INTERVAL_ID__) {
+    return;
+  }
+
+  clearInterval(cacheCleanupState.__CCH_CACHE_CLEANUP_INTERVAL_ID__);
+  cacheCleanupState.__CCH_CACHE_CLEANUP_INTERVAL_ID__ = null;
+}
+
 /**
  * 获取缓存统计信息
  */

+ 124 - 36
src/lib/logger.ts

@@ -1,4 +1,3 @@
-import pino from "pino";
 import { isDevelopment } from "./config/env.schema";
 
 /**
@@ -6,6 +5,25 @@ import { isDevelopment } from "./config/env.schema";
  */
 export type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
 
+type LoggerWrapper = {
+  fatal: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  error: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  warn: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  info: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  debug: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  trace: (arg1: unknown, arg2?: unknown, ...args: unknown[]) => void;
+  level: string;
+};
+
+const levelPriority: Record<LogLevel, number> = {
+  trace: 10,
+  debug: 20,
+  info: 30,
+  warn: 40,
+  error: 50,
+  fatal: 60,
+};
+
 /**
  * 获取初始日志级别
  * - 优先使用环境变量 LOG_LEVEL
@@ -28,36 +46,40 @@ function getInitialLogLevel(): LogLevel {
   return isDevelopment() ? "debug" : "info";
 }
 
-/**
- * 创建 Pino 日志实例
- * pino-pretty 在 Next.js 15 + Turbopack 下有兼容性问题,暂时禁用
- * 在 Turbopack 环境下使用默认格式化器(不启用 pino-pretty)
- */
-const enablePrettyTransport = isDevelopment() && !process.env.TURBOPACK;
-
-const pinoInstance = pino({
-  level: getInitialLogLevel(),
-  transport: enablePrettyTransport
-    ? {
-        target: "pino-pretty",
-        options: {
-          colorize: true,
-          translateTime: "SYS:standard",
-          ignore: "pid,hostname",
-        },
+function isValidLevel(level: string): level is LogLevel {
+  return level in levelPriority;
+}
+
+function createConsoleLogger(initialLevel: LogLevel): LoggerWrapper {
+  let currentLevel: LogLevel = initialLevel;
+
+  const shouldLog = (level: LogLevel) => levelPriority[level] >= levelPriority[currentLevel];
+  const wrap = (method: (...args: unknown[]) => void, level: LogLevel) => {
+    return (arg1: unknown, arg2?: unknown, ...args: unknown[]) => {
+      if (!shouldLog(level)) return;
+      method(arg1, arg2, ...args);
+    };
+  };
+
+  return {
+    fatal: wrap(console.error, "fatal"),
+    error: wrap(console.error, "error"),
+    warn: wrap(console.warn, "warn"),
+    info: wrap(console.info, "info"),
+    debug: wrap(console.debug, "debug"),
+    trace: wrap(console.trace, "trace"),
+    get level() {
+      return currentLevel;
+    },
+    set level(newLevel: string) {
+      if (isValidLevel(newLevel)) {
+        currentLevel = newLevel;
       }
-    : undefined,
-  // 生产环境格式化时间戳为 ISO 8601 格式
-  // timestamp 是顶级配置项,返回格式化的时间字符串
-  timestamp: enablePrettyTransport
-    ? undefined // pino-pretty 会处理时间格式
-    : pino.stdTimeFunctions.isoTime,
-  formatters: {
-    level: (label) => {
-      return { level: label };
     },
-  },
-});
+  };
+}
+
+type PinoLogger = import("pino").Logger;
 
 /**
  * 日志包装器 - 支持灵活的参数顺序
@@ -66,16 +88,16 @@ const pinoInstance = pino({
  * 1. logger.info(obj, msg) - pino 原生方式
  * 2. logger.info(msg, obj) - 便捷方式
  */
-function createLoggerWrapper(pinoLogger: pino.Logger) {
-  const wrap = (level: pino.Level) => {
+function createLoggerWrapper(pinoLogger: PinoLogger): LoggerWrapper {
+  const wrap = (level: LogLevel) => {
     return (arg1: unknown, arg2?: unknown, ...args: unknown[]) => {
       // 如果第一个参数是字符串,第二个参数是对象,自动交换
       if (typeof arg1 === "string" && arg2 && typeof arg2 === "object" && !Array.isArray(arg2)) {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        pinoLogger[level](arg2 as any, arg1 as any, ...args);
+        (pinoLogger[level] as any)(arg2 as any, arg1 as any, ...args);
       } else {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        pinoLogger[level](arg1 as any, arg2 as any, ...args);
+        (pinoLogger[level] as any)(arg1 as any, arg2 as any, ...args);
       }
     };
   };
@@ -96,14 +118,80 @@ function createLoggerWrapper(pinoLogger: pino.Logger) {
   };
 }
 
-export const logger = createLoggerWrapper(pinoInstance);
+const initialLevel = getInitialLogLevel();
+let activeLogger: LoggerWrapper = createConsoleLogger(initialLevel);
+
+async function initializePinoLogger(): Promise<void> {
+  if (typeof window !== "undefined") return;
+  if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs") return;
+
+  try {
+    const pinoModule = await import("pino");
+    const pino = pinoModule.default;
+    const stdTimeFunctions = pinoModule.stdTimeFunctions ?? pino.stdTimeFunctions;
+    const enablePrettyTransport = isDevelopment() && !process.env.TURBOPACK;
+
+    const pinoInstance = pino({
+      level: activeLogger.level,
+      transport: enablePrettyTransport
+        ? {
+            target: "pino-pretty",
+            options: {
+              colorize: true,
+              translateTime: "SYS:standard",
+              ignore: "pid,hostname",
+            },
+          }
+        : undefined,
+      // 生产环境格式化时间戳为 ISO 8601 格式
+      // timestamp 是顶级配置项,返回格式化的时间字符串
+      timestamp: enablePrettyTransport
+        ? undefined // pino-pretty 会处理时间格式
+        : stdTimeFunctions?.isoTime,
+      formatters: {
+        level: (label) => {
+          return { level: label };
+        },
+      },
+    });
+
+    activeLogger = createLoggerWrapper(pinoInstance);
+  } catch (error) {
+    activeLogger.warn("[Logger] Failed to initialize pino, falling back to console logging", {
+      error,
+    });
+  }
+}
+
+void initializePinoLogger();
+
+export const logger: LoggerWrapper = {
+  fatal: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.fatal(arg1, arg2, ...args),
+  error: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.error(arg1, arg2, ...args),
+  warn: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.warn(arg1, arg2, ...args),
+  info: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.info(arg1, arg2, ...args),
+  debug: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.debug(arg1, arg2, ...args),
+  trace: (arg1: unknown, arg2?: unknown, ...args: unknown[]) =>
+    activeLogger.trace(arg1, arg2, ...args),
+  get level() {
+    return activeLogger.level;
+  },
+  set level(newLevel: string) {
+    activeLogger.level = newLevel;
+  },
+};
 
 /**
  * 运行时动态调整日志级别
  * @param newLevel 新的日志级别
  */
 export function setLogLevel(newLevel: LogLevel): void {
-  pinoInstance.level = newLevel;
+  logger.level = newLevel;
   logger.info(`日志级别已调整为: ${newLevel}`);
 }
 
@@ -111,5 +199,5 @@ export function setLogLevel(newLevel: LogLevel): void {
  * 获取当前日志级别
  */
 export function getLogLevel(): string {
-  return pinoInstance.level;
+  return logger.level;
 }

+ 8 - 0
src/repository/message.ts

@@ -113,6 +113,8 @@ export async function updateMessageRequestDetails(
     cacheTtlApplied?: string | null;
     providerChain?: CreateMessageRequestData["provider_chain"];
     errorMessage?: string;
+    errorStack?: string; // 完整堆栈信息
+    errorCause?: string; // 嵌套错误原因(JSON 格式)
     model?: string; // ⭐ 新增:支持更新重定向后的模型名称
     providerId?: number; // ⭐ 新增:支持更新最终供应商ID(重试切换后)
     context1mApplied?: boolean; // 是否应用了1M上下文窗口
@@ -152,6 +154,12 @@ export async function updateMessageRequestDetails(
   if (details.errorMessage !== undefined) {
     updateData.errorMessage = details.errorMessage;
   }
+  if (details.errorStack !== undefined) {
+    updateData.errorStack = details.errorStack;
+  }
+  if (details.errorCause !== undefined) {
+    updateData.errorCause = details.errorCause;
+  }
   if (details.model !== undefined) {
     updateData.model = details.model;
   }