Selaa lähdekoodia

Feat/dashboard UI improvements (#657)

* feat(dashboard): enhance logs UI and provider circuit breaker display

- Add time range filter improvements with preset options
- Enhance provider chain popover with better error display
- Improve provider vendor view circuit breaker UI
- Add availability dashboard and provider endpoints display name tests
- Update i18n messages for provider chain and settings

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore: format code (feat-dashboard-ui-improvements-601d7b0)

* fix: make deriveDisplayNameFromDomain async for Server Actions compatibility

The file has "use server" directive, which requires all exported functions
to be async. This fixes the Next.js Turbopack build error:
"Server Actions must be async functions."

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21357337034

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* fix(ui): improve TagInput dropdown positioning inside Dialog

- Detect when TagInput is inside a Dialog and portal suggestions to dialog-content
- Use absolute positioning relative to dialog container instead of fixed viewport positioning
- Fix scroll/resize event listeners to target the correct scroll container
- Add unit tests for TagInput behavior inside Dialog

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore: format code (feat-dashboard-ui-improvements-65ea8e5)

* fix(db): prevent token count overflow by using bigint columns

Change token-related columns in message_request table from integer to
bigint to prevent overflow when storing large token counts. Also update
all SQL aggregation queries to use double precision instead of ::int
casting to avoid overflow during SUM operations.

Changes:
- Schema: inputTokens, outputTokens, cacheCreationInputTokens,
  cacheReadInputTokens, cacheCreation5mInputTokens,
  cacheCreation1hInputTokens now use bigint
- Schema: providerVendorId is now NOT NULL
- Queries: All token SUM operations use ::double precision
- Tests: Add unit tests to verify no ::int casting in token aggregations

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore: format code (feat-dashboard-ui-improvements-7e73f97)

* fix(repository): handle multiple API prefixes in domain name derivation

Extend deriveDisplayNameFromDomain to skip common API prefixes (api, v1,
v2, v3, www) when extracting display names from domains. This fixes
handling of multi-prefix domains like v1.api.anthropic.com.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* feat(dashboard): improve home layout with sidebar and responsive grid

- Refactor DashboardBento to use two-column layout with fixed-width
  sidebar (300px) for LiveSessionsPanel on admin view
- Update DashboardMain to remove max-w-7xl constraint on dashboard home
  page for wider content area
- Simplify ActiveSessionsSkeleton to match compact list style
- Add showTokensCost prop to SessionListItem and ActiveSessionsList
  for conditional token/cost display
- Fix router import to use i18n routing in ActiveSessionsList
- Add unit tests for layout behavior and SessionListItem props

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(dashboard): prevent decision chain overflow with text wrapping

Add overflow constraints to LogicTraceTab and StepCard components:
- Add min-w-0 to grid containers to allow content shrinking
- Add break-all to font-mono text for long IDs and URLs
- Add flex-wrap and truncate for provider name badges
- Add overflow-hidden to StepCard details container

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor(dashboard): improve home layout alignment and structure

- Add max-w-7xl constraint to dashboard home container for consistent alignment with header
- Restructure dashboard-bento into 3 independent sections: metrics, chart, leaderboards
- Remove nested grid structure that caused misalignment on desktop
- Change admin layout from 2-column (content + sidebar) to 4-column grid (3 leaderboards + live sessions)
- Remove colSpan/rowSpan from StatisticsChartCard for full-width independent rendering
- Add min-h-[280px] to LeaderboardCard for consistent card heights
- Update tests to match new layout structure

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: address code review feedback

- Remove redundant isDashboardHomePage conditional branch (now same as default)
- Remove trailing period from vendorAggregationRule for style consistency

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Ding 2 viikkoa sitten
vanhempi
sitoutus
090d5e4b08
57 muutettua tiedostoa jossa 4648 lisäystä ja 279 poistoa
  1. 6 0
      drizzle/0057_conscious_quicksilver.sql
  2. 2890 0
      drizzle/meta/0057_snapshot.json
  3. 7 0
      drizzle/meta/_journal.json
  4. 5 0
      messages/en/provider-chain.json
  5. 1 0
      messages/en/settings/providers/strings.json
  6. 5 0
      messages/ja/provider-chain.json
  7. 1 0
      messages/ja/settings/providers/strings.json
  8. 5 0
      messages/ru/provider-chain.json
  9. 1 0
      messages/ru/settings/providers/strings.json
  10. 5 0
      messages/zh-CN/provider-chain.json
  11. 1 0
      messages/zh-CN/settings/providers/strings.json
  12. 5 0
      messages/zh-TW/provider-chain.json
  13. 1 0
      messages/zh-TW/settings/providers/strings.json
  14. 12 12
      src/actions/my-usage.ts
  15. 16 1
      src/actions/system-config.ts
  16. 36 27
      src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx
  17. 1 1
      src/app/[locale]/dashboard/_components/bento/leaderboard-card.tsx
  18. 1 7
      src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx
  19. 3 1
      src/app/[locale]/dashboard/_components/dashboard-main.tsx
  20. 0 4
      src/app/[locale]/dashboard/availability/_components/availability-dashboard.tsx
  21. 22 37
      src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx
  22. 37 27
      src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
  23. 3 1
      src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/StepCard.tsx
  24. 37 16
      src/app/[locale]/dashboard/logs/_components/filters/time-filters.tsx
  25. 21 25
      src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx
  26. 50 0
      src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx
  27. 30 9
      src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
  28. 7 1
      src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  29. 11 2
      src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx
  30. 5 5
      src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
  31. 3 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
  32. 3 0
      src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx
  33. 6 4
      src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  34. 60 7
      src/app/[locale]/dashboard/logs/_utils/time-range.ts
  35. 3 0
      src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
  36. 3 0
      src/app/[locale]/my-usage/_components/usage-logs-section.tsx
  37. 4 2
      src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  38. 13 2
      src/app/[locale]/my-usage/page.tsx
  39. 46 14
      src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  40. 5 1
      src/components/customs/active-sessions-list.tsx
  41. 24 16
      src/components/customs/session-list-item.tsx
  42. 123 0
      src/components/ui/__tests__/tag-input-dialog.test.tsx
  43. 42 22
      src/components/ui/tag-input.tsx
  44. 12 9
      src/drizzle/schema.ts
  45. 13 13
      src/repository/key.ts
  46. 3 3
      src/repository/leaderboard.ts
  47. 20 4
      src/repository/provider-endpoints.ts
  48. 208 0
      tests/unit/actions/my-usage-token-aggregation.test.ts
  49. 104 0
      tests/unit/components/session-list-item.test.tsx
  50. 15 0
      tests/unit/dashboard-logs-time-range-utils.test.ts
  51. 20 4
      tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx
  52. 53 0
      tests/unit/dashboard-logs-warmup-ui.test.tsx
  53. 77 0
      tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx
  54. 228 0
      tests/unit/dashboard/dashboard-home-layout.test.tsx
  55. 152 0
      tests/unit/repository/key-usage-token-overflow.test.ts
  56. 27 0
      tests/unit/repository/provider-endpoints-display-name.test.ts
  57. 156 2
      tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx

+ 6 - 0
drizzle/0057_conscious_quicksilver.sql

@@ -0,0 +1,6 @@
+ALTER TABLE "message_request" ALTER COLUMN "input_tokens" SET DATA TYPE bigint;--> statement-breakpoint
+ALTER TABLE "message_request" ALTER COLUMN "output_tokens" SET DATA TYPE bigint;--> statement-breakpoint
+ALTER TABLE "message_request" ALTER COLUMN "cache_creation_input_tokens" SET DATA TYPE bigint;--> statement-breakpoint
+ALTER TABLE "message_request" ALTER COLUMN "cache_read_input_tokens" SET DATA TYPE bigint;--> statement-breakpoint
+ALTER TABLE "message_request" ALTER COLUMN "cache_creation_5m_input_tokens" SET DATA TYPE bigint;--> statement-breakpoint
+ALTER TABLE "message_request" ALTER COLUMN "cache_creation_1h_input_tokens" SET DATA TYPE bigint;

+ 2890 - 0
drizzle/meta/0057_snapshot.json

@@ -0,0 +1,2890 @@
+{
+  "id": "734153dd-5481-44cd-a7c6-7adfbc027232",
+  "prevId": "75eef188-0cac-4ae8-9deb-9b0db4f046c2",
+  "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(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(21, 15)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "request_sequence": {
+          "name": "request_sequence",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1
+        },
+        "provider_chain": {
+          "name": "provider_chain",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "api_type": {
+          "name": "api_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "endpoint": {
+          "name": "endpoint",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "original_model": {
+          "name": "original_model",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "ttfb_ms": {
+          "name": "ttfb_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_input_tokens": {
+          "name": "cache_creation_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_read_input_tokens": {
+          "name": "cache_read_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_5m_input_tokens": {
+          "name": "cache_creation_5m_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_creation_1h_input_tokens": {
+          "name": "cache_creation_1h_input_tokens",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_applied": {
+          "name": "cache_ttl_applied",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "context_1m_applied": {
+          "name": "context_1m_applied",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "special_settings": {
+          "name": "special_settings",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_stack": {
+          "name": "error_stack",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_cause": {
+          "name": "error_cause",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "blocked_reason": {
+          "name": "blocked_reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "user_agent": {
+          "name": "user_agent",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "messages_count": {
+          "name": "messages_count",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_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_id_prefix": {
+          "name": "idx_message_request_session_id_prefix",
+          "columns": [
+            {
+              "expression": "\"session_id\" varchar_pattern_ops",
+              "asc": true,
+              "isExpression": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL AND (\"message_request\".\"blocked_by\" IS NULL OR \"message_request\".\"blocked_by\" <> 'warmup')",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_session_seq": {
+          "name": "idx_message_request_session_seq",
+          "columns": [
+            {
+              "expression": "session_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "request_sequence",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_endpoint": {
+          "name": "idx_message_request_endpoint",
+          "columns": [
+            {
+              "expression": "endpoint",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_blocked_by": {
+          "name": "idx_message_request_blocked_by",
+          "columns": [
+            {
+              "expression": "blocked_by",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'litellm'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_source": {
+          "name": "idx_model_prices_source",
+          "columns": [
+            {
+              "expression": "source",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_settings": {
+      "name": "notification_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "use_legacy_mode": {
+          "name": "use_legacy_mode",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_enabled": {
+          "name": "circuit_breaker_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "circuit_breaker_webhook": {
+          "name": "circuit_breaker_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_enabled": {
+          "name": "daily_leaderboard_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "daily_leaderboard_webhook": {
+          "name": "daily_leaderboard_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_leaderboard_time": {
+          "name": "daily_leaderboard_time",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'09:00'"
+        },
+        "daily_leaderboard_top_n": {
+          "name": "daily_leaderboard_top_n",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "cost_alert_enabled": {
+          "name": "cost_alert_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "cost_alert_webhook": {
+          "name": "cost_alert_webhook",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_alert_threshold": {
+          "name": "cost_alert_threshold",
+          "type": "numeric(5, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0.80'"
+        },
+        "cost_alert_check_interval": {
+          "name": "cost_alert_check_interval",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.notification_target_bindings": {
+      "name": "notification_target_bindings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "notification_type": {
+          "name": "notification_type",
+          "type": "notification_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "target_id": {
+          "name": "target_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "schedule_cron": {
+          "name": "schedule_cron",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "schedule_timezone": {
+          "name": "schedule_timezone",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'Asia/Shanghai'"
+        },
+        "template_override": {
+          "name": "template_override",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "unique_notification_target_binding": {
+          "name": "unique_notification_target_binding",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_type": {
+          "name": "idx_notification_bindings_type",
+          "columns": [
+            {
+              "expression": "notification_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_notification_bindings_target": {
+          "name": "idx_notification_bindings_target",
+          "columns": [
+            {
+              "expression": "target_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "notification_target_bindings_target_id_webhook_targets_id_fk": {
+          "name": "notification_target_bindings_target_id_webhook_targets_id_fk",
+          "tableFrom": "notification_target_bindings",
+          "tableTo": "webhook_targets",
+          "columnsFrom": [
+            "target_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoint_probe_logs": {
+      "name": "provider_endpoint_probe_logs",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "endpoint_id": {
+          "name": "endpoint_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "source": {
+          "name": "source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'scheduled'"
+        },
+        "ok": {
+          "name": "ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "status_code": {
+          "name": "status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "latency_ms": {
+          "name": "latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_type": {
+          "name": "error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "error_message": {
+          "name": "error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_provider_endpoint_probe_logs_endpoint_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_endpoint_created_at",
+          "columns": [
+            {
+              "expression": "endpoint_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoint_probe_logs_created_at": {
+          "name": "idx_provider_endpoint_probe_logs_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk": {
+          "name": "provider_endpoint_probe_logs_endpoint_id_provider_endpoints_id_fk",
+          "tableFrom": "provider_endpoint_probe_logs",
+          "tableTo": "provider_endpoints",
+          "columnsFrom": [
+            "endpoint_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_endpoints": {
+      "name": "provider_endpoints",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "vendor_id": {
+          "name": "vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "label": {
+          "name": "label",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "sort_order": {
+          "name": "sort_order",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_probed_at": {
+          "name": "last_probed_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_ok": {
+          "name": "last_probe_ok",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_status_code": {
+          "name": "last_probe_status_code",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_latency_ms": {
+          "name": "last_probe_latency_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_type": {
+          "name": "last_probe_error_type",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_probe_error_message": {
+          "name": "last_probe_error_message",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "uniq_provider_endpoints_vendor_type_url": {
+          "name": "uniq_provider_endpoints_vendor_type_url",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "url",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_vendor_type": {
+          "name": "idx_provider_endpoints_vendor_type",
+          "columns": [
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_enabled": {
+          "name": "idx_provider_endpoints_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"provider_endpoints\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_created_at": {
+          "name": "idx_provider_endpoints_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_endpoints_deleted_at": {
+          "name": "idx_provider_endpoints_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "provider_endpoints_vendor_id_provider_vendors_id_fk": {
+          "name": "provider_endpoints_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "provider_endpoints",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.provider_vendors": {
+      "name": "provider_vendors",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "website_domain": {
+          "name": "website_domain",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "display_name": {
+          "name": "display_name",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "uniq_provider_vendors_website_domain": {
+          "name": "uniq_provider_vendors_website_domain",
+          "columns": [
+            {
+              "expression": "website_domain",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": true,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_provider_vendors_created_at": {
+          "name": "idx_provider_vendors_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_vendor_id": {
+          "name": "provider_vendor_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "cost_multiplier": {
+          "name": "cost_multiplier",
+          "type": "numeric(10, 4)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'1.0'"
+        },
+        "group_tag": {
+          "name": "group_tag",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'claude'"
+        },
+        "preserve_client_ip": {
+          "name": "preserve_client_ip",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "model_redirects": {
+          "name": "model_redirects",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'null'::jsonb"
+        },
+        "join_claude_pool": {
+          "name": "join_claude_pool",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "codex_instructions_strategy": {
+          "name": "codex_instructions_strategy",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'auto'"
+        },
+        "mcp_passthrough_type": {
+          "name": "mcp_passthrough_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'none'"
+        },
+        "mcp_passthrough_url": {
+          "name": "mcp_passthrough_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_daily_usd": {
+          "name": "limit_daily_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "total_cost_reset_at": {
+          "name": "total_cost_reset_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "max_retry_attempts": {
+          "name": "max_retry_attempts",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "circuit_breaker_failure_threshold": {
+          "name": "circuit_breaker_failure_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 5
+        },
+        "circuit_breaker_open_duration": {
+          "name": "circuit_breaker_open_duration",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 1800000
+        },
+        "circuit_breaker_half_open_success_threshold": {
+          "name": "circuit_breaker_half_open_success_threshold",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 2
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "first_byte_timeout_streaming_ms": {
+          "name": "first_byte_timeout_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "streaming_idle_timeout_ms": {
+          "name": "streaming_idle_timeout_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "request_timeout_non_streaming_ms": {
+          "name": "request_timeout_non_streaming_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "website_url": {
+          "name": "website_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "favicon_url": {
+          "name": "favicon_url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cache_ttl_preference": {
+          "name": "cache_ttl_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "context_1m_preference": {
+          "name": "context_1m_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_effort_preference": {
+          "name": "codex_reasoning_effort_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_reasoning_summary_preference": {
+          "name": "codex_reasoning_summary_preference",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_text_verbosity_preference": {
+          "name": "codex_text_verbosity_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "codex_parallel_tool_calls_preference": {
+          "name": "codex_parallel_tool_calls_preference",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "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": {}
+        },
+        "idx_providers_vendor_type": {
+          "name": "idx_providers_vendor_type",
+          "columns": [
+            {
+              "expression": "provider_vendor_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "provider_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {
+        "providers_provider_vendor_id_provider_vendors_id_fk": {
+          "name": "providers_provider_vendor_id_provider_vendors_id_fk",
+          "tableFrom": "providers",
+          "tableTo": "provider_vendors",
+          "columnsFrom": [
+            "provider_vendor_id"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "restrict",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.request_filters": {
+      "name": "request_filters",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "action": {
+          "name": "action",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "target": {
+          "name": "target",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "replacement": {
+          "name": "replacement",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "priority": {
+          "name": "priority",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "binding_type": {
+          "name": "binding_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'global'"
+        },
+        "provider_ids": {
+          "name": "provider_ids",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "group_tags": {
+          "name": "group_tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_request_filters_enabled": {
+          "name": "idx_request_filters_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "priority",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_scope": {
+          "name": "idx_request_filters_scope",
+          "columns": [
+            {
+              "expression": "scope",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_action": {
+          "name": "idx_request_filters_action",
+          "columns": [
+            {
+              "expression": "action",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_request_filters_binding": {
+          "name": "idx_request_filters_binding",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "binding_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.sensitive_words": {
+      "name": "sensitive_words",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "word": {
+          "name": "word",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "match_type": {
+          "name": "match_type",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'contains'"
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_sensitive_words_enabled": {
+          "name": "idx_sensitive_words_enabled",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "match_type",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_sensitive_words_created_at": {
+          "name": "idx_sensitive_words_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.system_settings": {
+      "name": "system_settings",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "site_title": {
+          "name": "site_title",
+          "type": "varchar(128)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'Claude Code Hub'"
+        },
+        "allow_global_usage_view": {
+          "name": "allow_global_usage_view",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "currency_display": {
+          "name": "currency_display",
+          "type": "varchar(10)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'USD'"
+        },
+        "billing_model_source": {
+          "name": "billing_model_source",
+          "type": "varchar(20)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'original'"
+        },
+        "enable_auto_cleanup": {
+          "name": "enable_auto_cleanup",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "cleanup_retention_days": {
+          "name": "cleanup_retention_days",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 30
+        },
+        "cleanup_schedule": {
+          "name": "cleanup_schedule",
+          "type": "varchar(50)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0 2 * * *'"
+        },
+        "cleanup_batch_size": {
+          "name": "cleanup_batch_size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 10000
+        },
+        "enable_client_version_check": {
+          "name": "enable_client_version_check",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "verbose_provider_error": {
+          "name": "verbose_provider_error",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_http2": {
+          "name": "enable_http2",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "intercept_anthropic_warmup_requests": {
+          "name": "intercept_anthropic_warmup_requests",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": false
+        },
+        "enable_thinking_signature_rectifier": {
+          "name": "enable_thinking_signature_rectifier",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_codex_session_id_completion": {
+          "name": "enable_codex_session_id_completion",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "enable_response_fixer": {
+          "name": "enable_response_fixer",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "response_fixer_config": {
+          "name": "response_fixer_config",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'{\"fixTruncatedJson\":true,\"fixSseFormat\":true,\"fixEncoding\":true,\"maxJsonDepth\":200,\"maxFixSize\":1048576}'::jsonb"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "provider_group": {
+          "name": "provider_group",
+          "type": "varchar(200)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'default'"
+        },
+        "tags": {
+          "name": "tags",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "limit_5h_usd": {
+          "name": "limit_5h_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_weekly_usd": {
+          "name": "limit_weekly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_monthly_usd": {
+          "name": "limit_monthly_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_total_usd": {
+          "name": "limit_total_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "limit_concurrent_sessions": {
+          "name": "limit_concurrent_sessions",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "daily_reset_mode": {
+          "name": "daily_reset_mode",
+          "type": "daily_reset_mode",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'fixed'"
+        },
+        "daily_reset_time": {
+          "name": "daily_reset_time",
+          "type": "varchar(5)",
+          "primaryKey": false,
+          "notNull": true,
+          "default": "'00:00'"
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "allowed_clients": {
+          "name": "allowed_clients",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "allowed_models": {
+          "name": "allowed_models",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'[]'::jsonb"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_enabled_expires_at": {
+          "name": "idx_users_enabled_expires_at",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "expires_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.webhook_targets": {
+      "name": "webhook_targets",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(100)",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_type": {
+          "name": "provider_type",
+          "type": "webhook_provider_type",
+          "typeSchema": "public",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "webhook_url": {
+          "name": "webhook_url",
+          "type": "varchar(1024)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_bot_token": {
+          "name": "telegram_bot_token",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "telegram_chat_id": {
+          "name": "telegram_chat_id",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "dingtalk_secret": {
+          "name": "dingtalk_secret",
+          "type": "varchar(256)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_template": {
+          "name": "custom_template",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "custom_headers": {
+          "name": "custom_headers",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_url": {
+          "name": "proxy_url",
+          "type": "varchar(512)",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "proxy_fallback_to_direct": {
+          "name": "proxy_fallback_to_direct",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": false
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "last_test_at": {
+          "name": "last_test_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "last_test_result": {
+          "name": "last_test_result",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {
+    "public.daily_reset_mode": {
+      "name": "daily_reset_mode",
+      "schema": "public",
+      "values": [
+        "fixed",
+        "rolling"
+      ]
+    },
+    "public.notification_type": {
+      "name": "notification_type",
+      "schema": "public",
+      "values": [
+        "circuit_breaker",
+        "daily_leaderboard",
+        "cost_alert"
+      ]
+    },
+    "public.webhook_provider_type": {
+      "name": "webhook_provider_type",
+      "schema": "public",
+      "values": [
+        "wechat",
+        "feishu",
+        "dingtalk",
+        "telegram",
+        "custom"
+      ]
+    }
+  },
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 7 - 0
drizzle/meta/_journal.json

@@ -400,6 +400,13 @@
       "when": 1769008812140,
       "tag": "0056_tidy_quasar",
       "breakpoints": true
+    },
+    {
+      "idx": 57,
+      "version": "7",
+      "when": 1769446927761,
+      "tag": "0057_conscious_quicksilver",
+      "breakpoints": true
     }
   ]
 }

+ 5 - 0
messages/en/provider-chain.json

@@ -54,6 +54,11 @@
     "rate_limited": "Rate Limited",
     "circuit_open": "Circuit Open",
     "disabled": "Disabled",
+    "excluded": "Excluded",
+    "format_type_mismatch": "Format Type Mismatch",
+    "type_mismatch": "Type Mismatch",
+    "model_not_allowed": "Model Not Allowed",
+    "context_1m_disabled": "1M Context Disabled",
     "model_not_supported": "Model Not Supported",
     "group_mismatch": "Group Mismatch",
     "health_check_failed": "Health Check Failed"

+ 1 - 0
messages/en/settings/providers/strings.json

@@ -65,6 +65,7 @@
   "circuitStatus": "Circuit Status",
   "vendorTypeCircuit": "Vendor Type Circuit",
   "vendorFallbackName": "Vendor #{id}",
+  "vendorAggregationRule": "Grouped by website domain",
   "orphanedProviders": "Unknown Vendor",
   "vendorTypeCircuitUpdated": "Vendor type circuit updated",
   "noEndpoints": "No endpoints configured",

+ 5 - 0
messages/ja/provider-chain.json

@@ -54,6 +54,11 @@
     "rate_limited": "レート制限",
     "circuit_open": "サーキットオープン",
     "disabled": "無効",
+    "excluded": "除外済み",
+    "format_type_mismatch": "フォーマット不一致",
+    "type_mismatch": "タイプ不一致",
+    "model_not_allowed": "モデル不許可",
+    "context_1m_disabled": "1Mコンテキスト無効",
     "model_not_supported": "モデル非対応",
     "group_mismatch": "グループ不一致",
     "health_check_failed": "ヘルスチェック失敗"

+ 1 - 0
messages/ja/settings/providers/strings.json

@@ -65,6 +65,7 @@
   "circuitStatus": "回路状態",
   "vendorTypeCircuit": "ベンダー種別回路",
   "vendorFallbackName": "ベンダー #{id}",
+  "vendorAggregationRule": "公式ドメインで集約",
   "orphanedProviders": "不明なベンダー",
   "vendorTypeCircuitUpdated": "ベンダータイプサーキットが更新されました",
   "noEndpoints": "エンドポイントが設定されていません",

+ 5 - 0
messages/ru/provider-chain.json

@@ -54,6 +54,11 @@
     "rate_limited": "Ограничение скорости",
     "circuit_open": "Автомат открыт",
     "disabled": "Отключен",
+    "excluded": "Исключен",
+    "format_type_mismatch": "Несоответствие формата",
+    "type_mismatch": "Несоответствие типа",
+    "model_not_allowed": "Модель не разрешена",
+    "context_1m_disabled": "1M контекст отключен",
     "model_not_supported": "Модель не поддерживается",
     "group_mismatch": "Несоответствие группы",
     "health_check_failed": "Проверка состояния не пройдена"

+ 1 - 0
messages/ru/settings/providers/strings.json

@@ -65,6 +65,7 @@
   "circuitStatus": "Состояние цепи",
   "vendorTypeCircuit": "Цепь по типу провайдера",
   "vendorFallbackName": "Поставщик #{id}",
+  "vendorAggregationRule": "Группировка по домену сайта",
   "orphanedProviders": "Неизвестный поставщик",
   "vendorTypeCircuitUpdated": "Цепь типа поставщика обновлена",
   "noEndpoints": "Эндпоинты не настроены",

+ 5 - 0
messages/zh-CN/provider-chain.json

@@ -54,6 +54,11 @@
     "rate_limited": "速率限制",
     "circuit_open": "熔断器打开",
     "disabled": "已禁用",
+    "excluded": "已排除",
+    "format_type_mismatch": "请求格式不兼容",
+    "type_mismatch": "类型不匹配",
+    "model_not_allowed": "模型不允许",
+    "context_1m_disabled": "1M上下文已禁用",
     "model_not_supported": "不支持该模型",
     "group_mismatch": "分组不匹配",
     "health_check_failed": "健康检查失败"

+ 1 - 0
messages/zh-CN/settings/providers/strings.json

@@ -65,6 +65,7 @@
   "circuitStatus": "熔断状态",
   "vendorTypeCircuit": "服务商类型熔断",
   "vendorFallbackName": "服务商 #{id}",
+  "vendorAggregationRule": "按官网域名聚合",
   "orphanedProviders": "未知服务商",
   "vendorTypeCircuitUpdated": "已更新服务商类型熔断器",
   "noEndpoints": "暂无端点配置",

+ 5 - 0
messages/zh-TW/provider-chain.json

@@ -54,6 +54,11 @@
     "rate_limited": "速率限制",
     "circuit_open": "熔斷器開啟",
     "disabled": "已停用",
+    "excluded": "已排除",
+    "format_type_mismatch": "請求格式不相容",
+    "type_mismatch": "類型不匹配",
+    "model_not_allowed": "模型不允許",
+    "context_1m_disabled": "1M上下文已停用",
     "model_not_supported": "不支援該模型",
     "group_mismatch": "分組不匹配",
     "health_check_failed": "健康檢查失敗"

+ 1 - 0
messages/zh-TW/settings/providers/strings.json

@@ -65,6 +65,7 @@
   "circuitStatus": "熔斷狀態",
   "vendorTypeCircuit": "供應商類型熔斷",
   "vendorFallbackName": "服務商 #{id}",
+  "vendorAggregationRule": "按官方網站網域聚合",
   "orphanedProviders": "未知服務商",
   "vendorTypeCircuitUpdated": "已更新服務商類型熔斷器",
   "noEndpoints": "尚未設定端點",

+ 12 - 12
src/actions/my-usage.ts

@@ -354,8 +354,8 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
     const [aggregate] = await db
       .select({
         calls: sql<number>`count(*)::int`,
-        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
+        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
         costUsd: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`,
       })
       .from(messageRequest)
@@ -375,8 +375,8 @@ export async function getMyTodayStats(): Promise<ActionResult<MyTodayStats>> {
         originalModel: messageRequest.originalModel,
         calls: sql<number>`count(*)::int`,
         costUsd: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`,
-        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
+        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
       })
       .from(messageRequest)
       .where(
@@ -604,10 +604,10 @@ export async function getMyStatsSummary(
         model: messageRequest.model,
         requests: sql<number>`count(*)::int`,
         cost: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`,
-        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
-        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`,
-        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`,
+        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
+        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::double precision`,
+        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::double precision`,
       })
       .from(messageRequest)
       .where(
@@ -628,10 +628,10 @@ export async function getMyStatsSummary(
         model: messageRequest.model,
         requests: sql<number>`count(*)::int`,
         cost: sql<string>`COALESCE(sum(${messageRequest.costUsd}), 0)`,
-        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
-        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`,
-        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`,
+        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
+        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::double precision`,
+        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::double precision`,
       })
       .from(messageRequest)
       .where(

+ 16 - 1
src/actions/system-config.ts

@@ -2,7 +2,7 @@
 
 import { revalidatePath } from "next/cache";
 import { getSession } from "@/lib/auth";
-import { invalidateSystemSettingsCache } from "@/lib/config";
+import { getEnvConfig, invalidateSystemSettingsCache } from "@/lib/config";
 import { logger } from "@/lib/logger";
 import { UpdateSystemSettingsSchema } from "@/lib/validation/schemas";
 import { getSystemSettings, updateSystemSettings } from "@/repository/system-config";
@@ -24,6 +24,21 @@ export async function fetchSystemSettings(): Promise<ActionResult<SystemSettings
   }
 }
 
+export async function getServerTimeZone(): Promise<ActionResult<{ timeZone: string }>> {
+  try {
+    const session = await getSession();
+    if (!session) {
+      return { ok: false, error: "未授权" };
+    }
+
+    const { TZ } = getEnvConfig();
+    return { ok: true, data: { timeZone: TZ } };
+  } catch (error) {
+    logger.error("获取时区失败:", error);
+    return { ok: false, error: "获取时区失败" };
+  }
+}
+
 export async function saveSystemSettings(formData: {
   // 所有字段均为可选,支持部分更新
   siteTitle?: string;

+ 36 - 27
src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx

@@ -9,6 +9,7 @@ import type { OverviewData } from "@/actions/overview";
 import { getOverviewData } from "@/actions/overview";
 import { getUserStatistics } from "@/actions/statistics";
 import type { CurrencyCode } from "@/lib/utils";
+import { cn } from "@/lib/utils";
 import { formatCurrency } from "@/lib/utils/currency";
 import type {
   LeaderboardEntry,
@@ -202,10 +203,9 @@ export function DashboardBento({
 
   return (
     <div className="space-y-6">
-      {/* Top Section: Metrics + Live Sessions */}
+      {/* Section 1: Metrics (Admin only) */}
       {isAdmin && (
         <BentoGrid>
-          {/* Metric Cards */}
           <BentoMetricCard
             title={t("metrics.concurrent")}
             value={metrics.concurrentSessions}
@@ -213,7 +213,11 @@ export function DashboardBento({
             accentColor="emerald"
             className="min-h-[120px]"
             comparisons={[
-              { value: metrics.recentMinuteRequests, label: t("metrics.rpm"), isPercentage: false },
+              {
+                value: metrics.recentMinuteRequests,
+                label: t("metrics.rpm"),
+                isPercentage: false,
+              },
             ]}
           />
           <BentoMetricCard
@@ -244,25 +248,26 @@ export function DashboardBento({
         </BentoGrid>
       )}
 
-      {/* Middle Section: Statistics Chart + Live Sessions (Admin) */}
-      <BentoGrid>
-        {/* Statistics Chart - 3 columns for admin, 4 columns for non-admin */}
-        {statistics && (
-          <StatisticsChartCard
-            data={statistics}
-            onTimeRangeChange={setTimeRange}
-            currencyCode={currencyCode}
-            colSpan={isAdmin ? 3 : 4}
-          />
-        )}
-
-        {/* Live Sessions Panel - Right sidebar, spans 2 rows */}
-        {isAdmin && (
-          <LiveSessionsPanel sessions={sessionsWithActivity} isLoading={sessionsLoading} />
-        )}
+      {/* Section 2: Statistics Chart - Full width */}
+      {statistics && (
+        <StatisticsChartCard
+          data={statistics}
+          onTimeRangeChange={setTimeRange}
+          currencyCode={currencyCode}
+        />
+      )}
 
-        {/* Leaderboard Cards - Below chart, 3 columns */}
-        {canViewLeaderboard && (
+      {/* Section 3: Leaderboards + Live Sessions */}
+      {canViewLeaderboard && (
+        <div
+          data-testid={isAdmin ? "dashboard-home-layout" : undefined}
+          className={cn(
+            "grid gap-6",
+            isAdmin
+              ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_280px]"
+              : "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
+          )}
+        >
           <LeaderboardCard
             title={tl("userRankings")}
             entries={userLeaderboard}
@@ -273,8 +278,6 @@ export function DashboardBento({
             maxItems={3}
             accentColor="primary"
           />
-        )}
-        {canViewLeaderboard && (
           <LeaderboardCard
             title={tl("providerRankings")}
             entries={providerLeaderboard}
@@ -285,8 +288,6 @@ export function DashboardBento({
             maxItems={3}
             accentColor="purple"
           />
-        )}
-        {canViewLeaderboard && (
           <LeaderboardCard
             title={tl("modelRankings")}
             entries={modelLeaderboard}
@@ -297,8 +298,16 @@ export function DashboardBento({
             maxItems={3}
             accentColor="blue"
           />
-        )}
-      </BentoGrid>
+
+          {isAdmin && (
+            <LiveSessionsPanel
+              data-testid="dashboard-home-sidebar"
+              sessions={sessionsWithActivity}
+              isLoading={sessionsLoading}
+            />
+          )}
+        </div>
+      )}
     </div>
   );
 }

+ 1 - 1
src/app/[locale]/dashboard/_components/bento/leaderboard-card.tsx

@@ -166,7 +166,7 @@ export function LeaderboardCard({
   const maxCost = Math.max(...entries.map((e) => e.totalCost), 0);
 
   return (
-    <BentoCard className={cn("flex flex-col", className)}>
+    <BentoCard className={cn("flex flex-col min-h-[280px]", className)}>
       {/* Header */}
       <div className="flex items-center justify-between mb-3">
         <h4 className="text-sm font-semibold">{title}</h4>

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

@@ -29,7 +29,6 @@ export interface StatisticsChartCardProps {
   data: UserStatisticsData;
   onTimeRangeChange?: (timeRange: TimeRange) => void;
   currencyCode?: CurrencyCode;
-  colSpan?: 3 | 4;
   className?: string;
 }
 
@@ -37,7 +36,6 @@ export function StatisticsChartCard({
   data,
   onTimeRangeChange,
   currencyCode = "USD",
-  colSpan = 4,
   className,
 }: StatisticsChartCardProps) {
   const t = useTranslations("dashboard.statistics");
@@ -175,11 +173,7 @@ export function StatisticsChartCard({
   };
 
   return (
-    <BentoCard
-      colSpan={colSpan}
-      rowSpan={2}
-      className={cn("flex flex-col p-0 overflow-hidden", className)}
-    >
+    <BentoCard className={cn("flex flex-col p-0 overflow-hidden", className)}>
       {/* Header */}
       <div className="flex items-center justify-between border-b border-border/50 dark:border-white/[0.06]">
         <div className="flex items-center gap-4 p-4">

+ 3 - 1
src/app/[locale]/dashboard/_components/dashboard-main.tsx

@@ -10,12 +10,14 @@ interface DashboardMainProps {
 export function DashboardMain({ children }: DashboardMainProps) {
   const pathname = usePathname();
 
+  const normalizedPathname = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
+
   // Pattern to match /dashboard/sessions/[id]/messages
   // The usePathname hook from next-intl/routing might return the path without locale prefix if configured that way,
   // or we just check for the suffix.
   // Let's be safe and check if it includes "/dashboard/sessions/" and ends with "/messages"
   const isSessionMessagesPage =
-    pathname.includes("/dashboard/sessions/") && pathname.endsWith("/messages");
+    normalizedPathname.includes("/dashboard/sessions/") && normalizedPathname.endsWith("/messages");
 
   if (isSessionMessagesPage) {
     return <main className="h-[calc(100vh-64px)] w-full overflow-hidden">{children}</main>;

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

@@ -8,7 +8,6 @@ import { cn } from "@/lib/utils";
 import { EndpointTab } from "./endpoint/endpoint-tab";
 import { OverviewSection } from "./overview/overview-section";
 import { ProviderTab } from "./provider/provider-tab";
-import { FloatingProbeButton } from "./shared/floating-probe-button";
 
 export type TimeRangeOption = "15min" | "1h" | "6h" | "24h" | "7d";
 
@@ -166,9 +165,6 @@ export function AvailabilityDashboard() {
           <EndpointTab />
         </TabsContent>
       </Tabs>
-
-      {/* Floating Probe Button */}
-      <FloatingProbeButton onProbeComplete={fetchData} />
     </div>
   );
 }

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

@@ -1,47 +1,32 @@
-import { Card, CardContent, CardHeader } from "@/components/ui/card";
 import { Skeleton } from "@/components/ui/skeleton";
 
-function CardSkeleton() {
+export function ActiveSessionsSkeleton() {
   return (
-    <Card className="w-[280px] shrink-0">
-      <CardContent className="p-4 space-y-3">
-        <div className="flex items-center justify-between">
-          <Skeleton className="h-5 w-24" />
-          <Skeleton className="h-5 w-16" />
-        </div>
-        <Skeleton className="h-4 w-40" />
-        <Skeleton className="h-4 w-32" />
-        <div className="flex items-center justify-between pt-2 border-t">
-          <Skeleton className="h-4 w-24" />
-          <Skeleton className="h-4 w-16" />
+    <div className="border rounded-lg bg-card">
+      <div className="px-4 py-3 border-b flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          <Skeleton className="h-4 w-4" />
+          <Skeleton className="h-4 w-28" />
+          <Skeleton className="h-3 w-40" />
         </div>
-      </CardContent>
-    </Card>
-  );
-}
+        <Skeleton className="h-3 w-20" />
+      </div>
 
-export function ActiveSessionsSkeleton() {
-  return (
-    <Card className="border-border/50">
-      <CardHeader className="pb-3">
-        <div className="flex items-center justify-between">
-          <div className="flex items-center gap-2">
-            <Skeleton className="h-8 w-8 rounded-lg" />
-            <div className="space-y-1">
-              <Skeleton className="h-5 w-28" />
-              <Skeleton className="h-3 w-40" />
+      <div style={{ maxHeight: "200px" }} className="overflow-y-auto">
+        <div className="divide-y">
+          {Array.from({ length: 5 }).map((_, idx) => (
+            <div key={idx} className="px-3 py-2">
+              <div className="flex items-center gap-2">
+                <Skeleton className="h-3.5 w-3.5 rounded-full" />
+                <Skeleton className="h-3 w-20" />
+                <Skeleton className="h-3 w-16" />
+                <Skeleton className="h-3 w-28" />
+                <Skeleton className="h-3 w-10 ml-auto" />
+              </div>
             </div>
-          </div>
-          <Skeleton className="h-4 w-20" />
-        </div>
-      </CardHeader>
-      <CardContent className="pt-0">
-        <div className="flex gap-3 pb-3 overflow-hidden">
-          {[1, 2, 3].map((i) => (
-            <CardSkeleton key={i} />
           ))}
         </div>
-      </CardContent>
-    </Card>
+      </div>
+    </div>
   );
 }

+ 37 - 27
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx

@@ -216,7 +216,7 @@ export function LogicTraceTab({
                       <Database className="h-3 w-3" />
                       <span className="font-medium">{t("logicTrace.sessionInfo")}</span>
                     </div>
-                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5 pl-4">
+                    <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5 pl-4 min-w-0">
                       {sessionReuseContext?.sessionId && (
                         <div className="flex items-center gap-2">
                           <span className="text-muted-foreground">
@@ -252,14 +252,14 @@ export function LogicTraceTab({
                       <Server className="h-3 w-3" />
                       <span className="font-medium">{t("logicTrace.reusedProvider")}</span>
                     </div>
-                    <div className="grid grid-cols-2 gap-1.5 pl-4">
-                      <div>
+                    <div className="grid grid-cols-2 gap-1.5 pl-4 min-w-0">
+                      <div className="min-w-0">
                         <span className="text-muted-foreground">Provider:</span>{" "}
-                        <span className="font-medium">{sessionReuseProvider.name}</span>
+                        <span className="font-medium break-all">{sessionReuseProvider.name}</span>
                       </div>
-                      <div>
+                      <div className="min-w-0">
                         <span className="text-muted-foreground">ID:</span>{" "}
-                        <span className="font-mono">{sessionReuseProvider.id}</span>
+                        <span className="font-mono break-all">{sessionReuseProvider.id}</span>
                       </div>
                       {sessionReuseProvider.priority !== undefined && (
                         <div>
@@ -301,23 +301,23 @@ export function LogicTraceTab({
               subtitle={`${decisionContext.totalProviders} -> ${decisionContext.afterModelFilter || decisionContext.afterHealthCheck}`}
               status="success"
               details={
-                <div className="grid grid-cols-2 gap-2 text-xs">
-                  <div>
+                <div className="grid grid-cols-2 gap-2 text-xs min-w-0">
+                  <div className="min-w-0">
                     <span className="text-muted-foreground">Total:</span>{" "}
                     <span className="font-mono">{decisionContext.totalProviders}</span>
                   </div>
-                  <div>
+                  <div className="min-w-0">
                     <span className="text-muted-foreground">Enabled:</span>{" "}
                     <span className="font-mono">{decisionContext.enabledProviders}</span>
                   </div>
                   {decisionContext.afterGroupFilter !== undefined && (
-                    <div>
+                    <div className="min-w-0">
                       <span className="text-muted-foreground">After Group:</span>{" "}
                       <span className="font-mono">{decisionContext.afterGroupFilter}</span>
                     </div>
                   )}
                   {decisionContext.afterModelFilter !== undefined && (
-                    <div>
+                    <div className="min-w-0">
                       <span className="text-muted-foreground">After Model:</span>{" "}
                       <span className="font-mono">{decisionContext.afterModelFilter}</span>
                     </div>
@@ -336,14 +336,24 @@ export function LogicTraceTab({
               subtitle={`${filteredProviders.length} providers filtered`}
               status="warning"
               details={
-                <div className="space-y-1">
+                <div className="space-y-1 min-w-0">
                   {filteredProviders.map((p, idx) => (
-                    <div key={`${p.id}-${idx}`} className="flex items-center gap-2 text-xs">
-                      <Badge variant="outline" className="text-[10px]">
+                    <div
+                      key={`${p.id}-${idx}`}
+                      className="flex items-center gap-2 text-xs flex-wrap min-w-0"
+                    >
+                      <Badge
+                        variant="outline"
+                        className="text-[10px] shrink-0 max-w-[120px] truncate"
+                      >
                         {p.name}
                       </Badge>
-                      <span className="text-rose-600">{tChain(`filterReasons.${p.reason}`)}</span>
-                      {p.details && <span className="text-muted-foreground">({p.details})</span>}
+                      <span className="text-rose-600 break-all">
+                        {tChain(`filterReasons.${p.reason}`)}
+                      </span>
+                      {p.details && (
+                        <span className="text-muted-foreground break-all">({p.details})</span>
+                      )}
                     </div>
                   ))}
                 </div>
@@ -468,13 +478,13 @@ export function LogicTraceTab({
                           <Link2 className="h-3 w-3" />
                           <span className="font-medium">{t("logicTrace.sessionReuseTitle")}</span>
                         </div>
-                        <div className="grid grid-cols-1 gap-1.5">
+                        <div className="grid grid-cols-1 gap-1.5 min-w-0">
                           {item.decisionContext.sessionId && (
-                            <div className="flex items-center gap-2">
-                              <span className="text-muted-foreground">
+                            <div className="flex items-center gap-2 min-w-0 flex-wrap">
+                              <span className="text-muted-foreground shrink-0">
                                 {tChain("timeline.sessionId", { id: "" }).replace(": ", ":")}
                               </span>
-                              <code className="text-[10px] px-1.5 py-0.5 bg-violet-100 dark:bg-violet-900/30 rounded font-mono">
+                              <code className="text-[10px] px-1.5 py-0.5 bg-violet-100 dark:bg-violet-900/30 rounded font-mono break-all">
                                 {item.decisionContext.sessionId}
                               </code>
                             </div>
@@ -487,23 +497,23 @@ export function LogicTraceTab({
                     )}
 
                     {/* Basic Info */}
-                    <div className="grid grid-cols-2 gap-2">
-                      <div>
+                    <div className="grid grid-cols-2 gap-2 min-w-0">
+                      <div className="min-w-0">
                         <span className="text-muted-foreground">Provider ID:</span>{" "}
-                        <span className="font-mono">{item.id}</span>
+                        <span className="font-mono break-all">{item.id}</span>
                       </div>
                       {item.selectionMethod && !isSessionReuse && (
-                        <div>
+                        <div className="min-w-0">
                           <span className="text-muted-foreground">
                             {tChain("details.selectionMethod")}:
                           </span>{" "}
-                          <span className="font-mono">{item.selectionMethod}</span>
+                          <span className="font-mono break-all">{item.selectionMethod}</span>
                         </div>
                       )}
                       {isSessionReuse && (
-                        <div>
+                        <div className="min-w-0">
                           <span className="text-muted-foreground">Provider:</span>{" "}
-                          <span className="font-mono">{item.name}</span>
+                          <span className="font-mono break-all">{item.name}</span>
                         </div>
                       )}
                     </div>

+ 3 - 1
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/StepCard.tsx

@@ -173,7 +173,9 @@ export function StepCard({
 
           {/* Expandable details */}
           {hasDetails && isExpanded && (
-            <div className="mt-3 pt-3 border-t border-current/10">{details}</div>
+            <div className="mt-3 pt-3 border-t border-current/10 overflow-hidden min-w-0">
+              {details}
+            </div>
           )}
         </div>
       </div>

+ 37 - 16
src/app/[locale]/dashboard/logs/_components/filters/time-filters.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import { format } from "date-fns";
+import { formatInTimeZone } from "date-fns-tz";
 import { useTranslations } from "next-intl";
 import { useCallback, useMemo } from "react";
 import { Input } from "@/components/ui/input";
@@ -16,16 +17,23 @@ import type { UsageLogFilters } from "./types";
 interface TimeFiltersProps {
   filters: UsageLogFilters;
   onFiltersChange: (filters: UsageLogFilters) => void;
+  serverTimeZone?: string;
 }
 
-export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
+export function TimeFilters({ filters, onFiltersChange, serverTimeZone }: TimeFiltersProps) {
   const t = useTranslations("dashboard.logs.filters");
 
   // Helper: convert timestamp to display date string (YYYY-MM-DD)
-  const timestampToDateString = useCallback((timestamp: number): string => {
-    const date = new Date(timestamp);
-    return format(date, "yyyy-MM-dd");
-  }, []);
+  const timestampToDateString = useCallback(
+    (timestamp: number): string => {
+      const date = new Date(timestamp);
+      if (serverTimeZone) {
+        return formatInTimeZone(date, serverTimeZone, "yyyy-MM-dd");
+      }
+      return format(date, "yyyy-MM-dd");
+    },
+    [serverTimeZone]
+  );
 
   // Memoized startDate for display (from timestamp)
   const displayStartDate = useMemo(() => {
@@ -35,8 +43,8 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
 
   const displayStartClock = useMemo(() => {
     if (!filters.startTime) return undefined;
-    return formatClockFromTimestamp(filters.startTime);
-  }, [filters.startTime]);
+    return formatClockFromTimestamp(filters.startTime, serverTimeZone);
+  }, [filters.startTime, serverTimeZone]);
 
   // Memoized endDate calculation: endTime is exclusive, use endTime-1s to infer inclusive display end date
   const displayEndDate = useMemo(() => {
@@ -48,8 +56,8 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
   const displayEndClock = useMemo(() => {
     if (!filters.endTime) return undefined;
     const inclusiveEndTime = inclusiveEndTimestampFromExclusive(filters.endTime);
-    return formatClockFromTimestamp(inclusiveEndTime);
-  }, [filters.endTime]);
+    return formatClockFromTimestamp(inclusiveEndTime, serverTimeZone);
+  }, [filters.endTime, serverTimeZone]);
 
   // Memoized callback for date range changes
   const handleDateRangeChange = useCallback(
@@ -57,8 +65,16 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
       if (range.startDate && range.endDate) {
         const startClock = displayStartClock ?? "00:00:00";
         const endClock = displayEndClock ?? "23:59:59";
-        const startTimestamp = dateStringWithClockToTimestamp(range.startDate, startClock);
-        const endInclusiveTimestamp = dateStringWithClockToTimestamp(range.endDate, endClock);
+        const startTimestamp = dateStringWithClockToTimestamp(
+          range.startDate,
+          startClock,
+          serverTimeZone
+        );
+        const endInclusiveTimestamp = dateStringWithClockToTimestamp(
+          range.endDate,
+          endClock,
+          serverTimeZone
+        );
         if (startTimestamp === undefined || endInclusiveTimestamp === undefined) {
           onFiltersChange({
             ...filters,
@@ -81,7 +97,7 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
         });
       }
     },
-    [displayEndClock, displayStartClock, filters, onFiltersChange]
+    [displayEndClock, displayStartClock, filters, onFiltersChange, serverTimeZone]
   );
 
   const handleStartTimeChange = useCallback(
@@ -89,14 +105,14 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
       const nextClock = e.target.value || "00:00:00";
       if (!filters.startTime) return;
       const dateStr = timestampToDateString(filters.startTime);
-      const startTime = dateStringWithClockToTimestamp(dateStr, nextClock);
+      const startTime = dateStringWithClockToTimestamp(dateStr, nextClock, serverTimeZone);
       if (startTime === undefined) return;
       onFiltersChange({
         ...filters,
         startTime,
       });
     },
-    [filters, onFiltersChange, timestampToDateString]
+    [filters, onFiltersChange, timestampToDateString, serverTimeZone]
   );
 
   const handleEndTimeChange = useCallback(
@@ -105,14 +121,18 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
       if (!filters.endTime) return;
       const inclusiveEndTime = inclusiveEndTimestampFromExclusive(filters.endTime);
       const endDateStr = timestampToDateString(inclusiveEndTime);
-      const endInclusiveTimestamp = dateStringWithClockToTimestamp(endDateStr, nextClock);
+      const endInclusiveTimestamp = dateStringWithClockToTimestamp(
+        endDateStr,
+        nextClock,
+        serverTimeZone
+      );
       if (endInclusiveTimestamp === undefined) return;
       onFiltersChange({
         ...filters,
         endTime: endInclusiveTimestamp + 1000,
       });
     },
-    [filters, onFiltersChange, timestampToDateString]
+    [filters, onFiltersChange, timestampToDateString, serverTimeZone]
   );
 
   return (
@@ -123,6 +143,7 @@ export function TimeFilters({ filters, onFiltersChange }: TimeFiltersProps) {
           startDate={displayStartDate}
           endDate={displayEndDate}
           onDateRangeChange={handleDateRangeChange}
+          serverTimeZone={serverTimeZone}
         />
       </div>
       <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">

+ 21 - 25
src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { addDays, differenceInCalendarDays, format, subDays } from "date-fns";
+import { addDays, differenceInCalendarDays, format } from "date-fns";
 import { CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { useCallback, useMemo, useState } from "react";
@@ -9,16 +9,18 @@ import { Button } from "@/components/ui/button";
 import { Calendar } from "@/components/ui/calendar";
 import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import { cn } from "@/lib/utils";
+import { getQuickDateRange, type QuickPeriod } from "../_utils/time-range";
 
 interface LogsDateRangePickerProps {
   startDate?: string; // "YYYY-MM-DD"
   endDate?: string; // "YYYY-MM-DD"
   onDateRangeChange: (range: { startDate?: string; endDate?: string }) => void;
+  serverTimeZone?: string;
 }
 
-type QuickPeriod = "today" | "yesterday" | "last7days" | "last30days" | "custom";
+type PickerQuickPeriod = QuickPeriod | "custom";
 
-const QUICK_PERIODS: Exclude<QuickPeriod, "custom">[] = [
+const QUICK_PERIODS: Exclude<PickerQuickPeriod, "custom">[] = [
   "today",
   "yesterday",
   "last7days",
@@ -35,29 +37,22 @@ function parseDate(dateStr: string): Date {
   return new Date(year, month - 1, day);
 }
 
-function getDateRangeForPeriod(period: QuickPeriod): { startDate: string; endDate: string } {
-  const today = new Date();
-  switch (period) {
-    case "today":
-      return { startDate: formatDate(today), endDate: formatDate(today) };
-    case "yesterday": {
-      const yesterday = subDays(today, 1);
-      return { startDate: formatDate(yesterday), endDate: formatDate(yesterday) };
-    }
-    case "last7days":
-      return { startDate: formatDate(subDays(today, 6)), endDate: formatDate(today) };
-    case "last30days":
-      return { startDate: formatDate(subDays(today, 29)), endDate: formatDate(today) };
-    default:
-      return { startDate: formatDate(today), endDate: formatDate(today) };
-  }
+function getDateRangeForPeriod(
+  period: QuickPeriod,
+  serverTimeZone?: string
+): { startDate: string; endDate: string } {
+  return getQuickDateRange(period, serverTimeZone);
 }
 
-function detectQuickPeriod(startDate?: string, endDate?: string): QuickPeriod | null {
+function detectQuickPeriod(
+  startDate?: string,
+  endDate?: string,
+  serverTimeZone?: string
+): PickerQuickPeriod | null {
   if (!startDate || !endDate) return null;
 
   for (const period of QUICK_PERIODS) {
-    const range = getDateRangeForPeriod(period);
+    const range = getDateRangeForPeriod(period, serverTimeZone);
     if (range.startDate === startDate && range.endDate === endDate) {
       return period;
     }
@@ -85,6 +80,7 @@ export function LogsDateRangePicker({
   startDate,
   endDate,
   onDateRangeChange,
+  serverTimeZone,
 }: LogsDateRangePickerProps) {
   const t = useTranslations("dashboard");
   const tCommon = useTranslations("common");
@@ -93,8 +89,8 @@ export function LogsDateRangePicker({
   const hasDateRange = Boolean(startDate && endDate);
 
   const activeQuickPeriod = useMemo(() => {
-    return detectQuickPeriod(startDate, endDate);
-  }, [startDate, endDate]);
+    return detectQuickPeriod(startDate, endDate, serverTimeZone);
+  }, [startDate, endDate, serverTimeZone]);
 
   const selectedRange: DateRange | undefined = useMemo(() => {
     if (!startDate || !endDate) return undefined;
@@ -106,10 +102,10 @@ export function LogsDateRangePicker({
 
   const handleQuickPeriodClick = useCallback(
     (period: QuickPeriod) => {
-      const range = getDateRangeForPeriod(period);
+      const range = getDateRangeForPeriod(period, serverTimeZone);
       onDateRangeChange(range);
     },
-    [onDateRangeChange]
+    [onDateRangeChange, serverTimeZone]
   );
 
   const handleNavigate = useCallback(

+ 50 - 0
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.test.tsx

@@ -205,6 +205,56 @@ describe("provider-chain-popover probability formatting", () => {
   });
 });
 
+describe("provider-chain-popover group badges", () => {
+  test("renders multiple deduped group badges with tooltip content", () => {
+    const html = renderWithIntl(
+      <ProviderChainPopover
+        chain={[
+          {
+            id: 1,
+            name: "p1",
+            reason: "initial_selection",
+            decisionContext: {
+              totalProviders: 1,
+              enabledProviders: 1,
+              targetType: "claude",
+              groupFilterApplied: false,
+              beforeHealthCheck: 1,
+              afterHealthCheck: 1,
+              priorityLevels: [1],
+              selectedPriority: 1,
+              candidatesAtPriority: [{ id: 1, name: "p1", weight: 100, costMultiplier: 1 }],
+            },
+          },
+          {
+            id: 2,
+            name: "p1",
+            reason: "retry_failed",
+            statusCode: 500,
+          },
+          {
+            id: 3,
+            name: "p1",
+            reason: "request_success",
+            statusCode: 200,
+            groupTag: "alpha, beta, alpha",
+          },
+        ]}
+        finalProvider="p1"
+      />
+    );
+
+    const document = parseHtml(html);
+    const badgeTexts = Array.from(document.querySelectorAll("[data-slot='badge']")).map(
+      (node) => node.textContent
+    );
+    expect(badgeTexts.filter((text) => text === "alpha").length).toBe(1);
+    expect(badgeTexts.filter((text) => text === "beta").length).toBe(1);
+    expect(document.body.textContent).toContain("alpha");
+    expect(document.body.textContent).toContain("beta");
+  });
+});
+
 describe("provider-chain-popover layout", () => {
   test("requestCount<=1 branch keeps truncation container shrinkable", () => {
     const html = renderWithIntl(

+ 30 - 9
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx

@@ -38,6 +38,19 @@ function isActualRequest(item: ProviderChainItem): boolean {
   return false;
 }
 
+function parseGroupTags(groupTag?: string | null): string[] {
+  if (!groupTag) return [];
+  const seen = new Set<string>();
+  const groups: string[] = [];
+  for (const raw of groupTag.split(",")) {
+    const trimmed = raw.trim();
+    if (!trimmed || seen.has(trimmed)) continue;
+    seen.add(trimmed);
+    groups.push(trimmed);
+  }
+  return groups;
+}
+
 /**
  * Get status icon and color for a provider chain item
  */
@@ -279,6 +292,7 @@ export function ProviderChainPopover({
     .find((item) => item.reason === "request_success" || item.reason === "retry_success");
   const finalCostMultiplier = successfulProvider?.costMultiplier;
   const finalGroupTag = successfulProvider?.groupTag;
+  const finalGroupTags = parseGroupTags(finalGroupTag);
   const hasFinalCostBadge =
     finalCostMultiplier !== undefined &&
     finalCostMultiplier !== null &&
@@ -318,15 +332,22 @@ export function ProviderChainPopover({
                 x{finalCostMultiplier.toFixed(2)}
               </Badge>
             )}
-            {/* Group tag badge (if present) */}
-            {finalGroupTag && (
-              <Badge
-                variant="outline"
-                className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700"
-              >
-                {finalGroupTag}
-              </Badge>
-            )}
+            {/* Group tag badges (if present) */}
+            {finalGroupTags.map((group) => (
+              <TooltipProvider key={group}>
+                <Tooltip delayDuration={200}>
+                  <TooltipTrigger asChild>
+                    <Badge
+                      variant="outline"
+                      className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700 max-w-[120px] truncate"
+                    >
+                      {group}
+                    </Badge>
+                  </TooltipTrigger>
+                  <TooltipContent>{group}</TooltipContent>
+                </Tooltip>
+              </TooltipProvider>
+            ))}
             {/* Info icon */}
             <InfoIcon className="h-3 w-3 text-muted-foreground shrink-0" aria-hidden="true" />
           </span>

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

@@ -52,6 +52,7 @@ interface UsageLogsFiltersProps {
   filters: UsageLogFilters;
   onChange: (filters: UsageLogFilters) => void;
   onReset: () => void;
+  serverTimeZone?: string;
 }
 
 export function UsageLogsFilters({
@@ -63,6 +64,7 @@ export function UsageLogsFilters({
   filters,
   onChange,
   onReset,
+  serverTimeZone,
 }: UsageLogsFiltersProps) {
   const t = useTranslations("dashboard");
 
@@ -259,7 +261,11 @@ export function UsageLogsFilters({
           activeCount={timeActiveCount}
           defaultOpen={true}
         >
-          <TimeFilters filters={localFilters} onFiltersChange={setLocalFilters} />
+          <TimeFilters
+            filters={localFilters}
+            onFiltersChange={setLocalFilters}
+            serverTimeZone={serverTimeZone}
+          />
         </FilterSection>
 
         {/* Identity Section (Admin only for User, all for Key) */}

+ 11 - 2
src/app/[locale]/dashboard/logs/_components/usage-logs-sections.tsx

@@ -1,5 +1,6 @@
 import { cache } from "react";
-import { ActiveSessionsCards } from "@/components/customs/active-sessions-cards";
+import { ActiveSessionsList } from "@/components/customs/active-sessions-list";
+import { getEnvConfig } from "@/lib/config";
 import { getSystemSettings } from "@/repository/system-config";
 import { UsageLogsViewVirtualized } from "./usage-logs-view-virtualized";
 
@@ -13,7 +14,13 @@ interface UsageLogsDataSectionProps {
 
 export async function UsageLogsActiveSessionsSection() {
   const systemSettings = await getCachedSystemSettings();
-  return <ActiveSessionsCards currencyCode={systemSettings.currencyDisplay} />;
+  return (
+    <ActiveSessionsList
+      currencyCode={systemSettings.currencyDisplay}
+      maxHeight="200px"
+      showTokensCost={false}
+    />
+  );
 }
 
 export async function UsageLogsDataSection({
@@ -22,12 +29,14 @@ export async function UsageLogsDataSection({
   searchParams,
 }: UsageLogsDataSectionProps) {
   const resolvedSearchParams = await searchParams;
+  const { TZ } = getEnvConfig();
 
   return (
     <UsageLogsViewVirtualized
       isAdmin={isAdmin}
       userId={userId}
       searchParams={resolvedSearchParams}
+      serverTimeZone={TZ}
     />
   );
 }

+ 5 - 5
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -287,16 +287,16 @@ export function UsageLogsTable({
                       <TooltipProvider>
                         <Tooltip delayDuration={250}>
                           <TooltipTrigger asChild>
-                            <div className="flex items-center justify-end gap-1 cursor-help">
-                              <span>
-                                {formatTokenAmount(log.cacheCreationInputTokens)} /{" "}
-                                {formatTokenAmount(log.cacheReadInputTokens)}
-                              </span>
+                            <div className="flex items-center gap-2 w-full cursor-help">
                               {log.cacheTtlApplied ? (
                                 <Badge variant="outline" className="text-[10px] leading-tight px-1">
                                   {log.cacheTtlApplied}
                                 </Badge>
                               ) : null}
+                              <span className="ml-auto">
+                                {formatTokenAmount(log.cacheCreationInputTokens)} /{" "}
+                                {formatTokenAmount(log.cacheReadInputTokens)}
+                              </span>
                             </div>
                           </TooltipTrigger>
                           <TooltipContent align="end" className="text-xs space-y-1">

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

@@ -43,6 +43,7 @@ interface UsageLogsViewVirtualizedProps {
   searchParams: { [key: string]: string | string[] | undefined };
   currencyCode?: CurrencyCode;
   billingModelSource?: BillingModelSource;
+  serverTimeZone?: string;
 }
 
 async function fetchSystemSettings(): Promise<SystemSettings> {
@@ -69,6 +70,7 @@ function UsageLogsViewContent({
   searchParams,
   currencyCode = "USD",
   billingModelSource = "original",
+  serverTimeZone,
 }: UsageLogsViewVirtualizedProps) {
   const t = useTranslations("dashboard");
   const tc = useTranslations("customs");
@@ -311,6 +313,7 @@ function UsageLogsViewContent({
               onReset={() => router.push("/dashboard/logs")}
               isProvidersLoading={isProvidersLoading}
               isKeysLoading={isKeysLoading}
+              serverTimeZone={serverTimeZone}
             />
           </CardContent>
         </Card>

+ 3 - 0
src/app/[locale]/dashboard/logs/_components/usage-logs-view.tsx

@@ -25,6 +25,7 @@ interface UsageLogsViewProps {
   searchParams: { [key: string]: string | string[] | undefined };
   currencyCode?: CurrencyCode;
   billingModelSource?: BillingModelSource;
+  serverTimeZone?: string;
 }
 
 export function UsageLogsView({
@@ -34,6 +35,7 @@ export function UsageLogsView({
   searchParams,
   currencyCode = "USD",
   billingModelSource = "original",
+  serverTimeZone,
 }: UsageLogsViewProps) {
   const t = useTranslations("dashboard");
   const router = useRouter();
@@ -190,6 +192,7 @@ export function UsageLogsView({
             filters={filters}
             onChange={handleFilterChange}
             onReset={() => router.push("/dashboard/logs")}
+            serverTimeZone={serverTimeZone}
           />
         </CardContent>
       </Card>

+ 6 - 4
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx

@@ -527,8 +527,8 @@ export function VirtualizedLogsTable({
                         <TooltipProvider>
                           <Tooltip delayDuration={250}>
                             <TooltipTrigger asChild>
-                              <div className="cursor-help flex flex-col items-end leading-tight tabular-nums">
-                                <div className="flex items-center gap-1">
+                              <div className="cursor-help flex flex-col w-full leading-tight tabular-nums">
+                                <div className="flex items-center gap-1 w-full">
                                   {log.cacheTtlApplied ? (
                                     <Badge
                                       variant="outline"
@@ -537,9 +537,11 @@ export function VirtualizedLogsTable({
                                       {log.cacheTtlApplied}
                                     </Badge>
                                   ) : null}
-                                  <span>{formatTokenAmount(log.cacheCreationInputTokens)}</span>
+                                  <span className="ml-auto text-right">
+                                    {formatTokenAmount(log.cacheCreationInputTokens)}
+                                  </span>
                                 </div>
-                                <span className="text-muted-foreground">
+                                <span className="text-muted-foreground text-right">
                                   {formatTokenAmount(log.cacheReadInputTokens)}
                                 </span>
                               </div>

+ 60 - 7
src/app/[locale]/dashboard/logs/_utils/time-range.ts

@@ -1,3 +1,6 @@
+import { format, subDays } from "date-fns";
+import { formatInTimeZone, fromZonedTime, toZonedTime } from "date-fns-tz";
+
 export interface ClockParts {
   hours: number;
   minutes: number;
@@ -18,8 +21,9 @@ export function parseClockString(clockStr: string): ClockParts {
   };
 }
 
-export function formatClockFromTimestamp(timestamp: number): string {
-  const date = new Date(timestamp);
+export function formatClockFromTimestamp(timestamp: number, timeZone?: string): string {
+  const baseDate = new Date(timestamp);
+  const date = timeZone ? toZonedTime(baseDate, timeZone) : baseDate;
   const hh = `${date.getHours()}`.padStart(2, "0");
   const mm = `${date.getMinutes()}`.padStart(2, "0");
   const ss = `${date.getSeconds()}`.padStart(2, "0");
@@ -28,18 +32,21 @@ export function formatClockFromTimestamp(timestamp: number): string {
 
 export function dateStringWithClockToTimestamp(
   dateStr: string,
-  clockStr: string
+  clockStr: string,
+  timeZone?: string
 ): number | undefined {
   const [year, month, day] = dateStr.split("-").map(Number);
   const { hours, minutes, seconds } = parseClockString(clockStr);
 
-  const date = new Date(year, month - 1, day, hours, minutes, seconds, 0);
+  const baseDate = new Date(year, month - 1, day, hours, minutes, seconds, 0);
+  const date = timeZone ? fromZonedTime(baseDate, timeZone) : baseDate;
   const timestamp = date.getTime();
   if (!Number.isFinite(timestamp)) return undefined;
 
-  if (date.getFullYear() !== year) return undefined;
-  if (date.getMonth() !== month - 1) return undefined;
-  if (date.getDate() !== day) return undefined;
+  const validationDate = timeZone ? toZonedTime(date, timeZone) : date;
+  if (validationDate.getFullYear() !== year) return undefined;
+  if (validationDate.getMonth() !== month - 1) return undefined;
+  if (validationDate.getDate() !== day) return undefined;
 
   return timestamp;
 }
@@ -47,3 +54,49 @@ export function dateStringWithClockToTimestamp(
 export function inclusiveEndTimestampFromExclusive(endExclusiveTimestamp: number): number {
   return Math.max(0, endExclusiveTimestamp - 1000);
 }
+
+export type QuickPeriod = "today" | "yesterday" | "last7days" | "last30days";
+
+function formatDateInTimeZone(date: Date, timeZone?: string): string {
+  if (timeZone) {
+    return formatInTimeZone(date, timeZone, "yyyy-MM-dd");
+  }
+  return format(date, "yyyy-MM-dd");
+}
+
+export function getQuickDateRange(
+  period: QuickPeriod,
+  timeZone?: string,
+  now: Date = new Date()
+): { startDate: string; endDate: string } {
+  const baseDate = timeZone ? toZonedTime(now, timeZone) : now;
+  switch (period) {
+    case "today":
+      return {
+        startDate: formatDateInTimeZone(baseDate, timeZone),
+        endDate: formatDateInTimeZone(baseDate, timeZone),
+      };
+    case "yesterday": {
+      const yesterday = subDays(baseDate, 1);
+      return {
+        startDate: formatDateInTimeZone(yesterday, timeZone),
+        endDate: formatDateInTimeZone(yesterday, timeZone),
+      };
+    }
+    case "last7days":
+      return {
+        startDate: formatDateInTimeZone(subDays(baseDate, 6), timeZone),
+        endDate: formatDateInTimeZone(baseDate, timeZone),
+      };
+    case "last30days":
+      return {
+        startDate: formatDateInTimeZone(subDays(baseDate, 29), timeZone),
+        endDate: formatDateInTimeZone(baseDate, timeZone),
+      };
+    default:
+      return {
+        startDate: formatDateInTimeZone(baseDate, timeZone),
+        endDate: formatDateInTimeZone(baseDate, timeZone),
+      };
+  }
+}

+ 3 - 0
src/app/[locale]/my-usage/_components/statistics-summary-card.tsx

@@ -28,11 +28,13 @@ import { LogsDateRangePicker } from "../../dashboard/logs/_components/logs-date-
 interface StatisticsSummaryCardProps {
   className?: string;
   autoRefreshSeconds?: number;
+  serverTimeZone?: string;
 }
 
 export function StatisticsSummaryCard({
   className,
   autoRefreshSeconds = 30,
+  serverTimeZone,
 }: StatisticsSummaryCardProps) {
   const t = useTranslations("myUsage.stats");
   const [stats, setStats] = useState<MyStatsSummary | null>(null);
@@ -128,6 +130,7 @@ export function StatisticsSummaryCard({
             startDate={dateRange.startDate}
             endDate={dateRange.endDate}
             onDateRangeChange={handleDateRangeChange}
+            serverTimeZone={serverTimeZone}
           />
           <Button
             size="sm"

+ 3 - 0
src/app/[locale]/my-usage/_components/usage-logs-section.tsx

@@ -30,6 +30,7 @@ interface UsageLogsSectionProps {
   loading?: boolean;
   autoRefreshSeconds?: number;
   defaultOpen?: boolean;
+  serverTimeZone?: string;
 }
 
 interface Filters {
@@ -48,6 +49,7 @@ export function UsageLogsSection({
   loading = false,
   autoRefreshSeconds,
   defaultOpen = false,
+  serverTimeZone,
 }: UsageLogsSectionProps) {
   const t = useTranslations("myUsage.logs");
   const tCollapsible = useTranslations("myUsage.logsCollapsible");
@@ -375,6 +377,7 @@ export function UsageLogsSection({
                   startDate={draftFilters.startDate}
                   endDate={draftFilters.endDate}
                   onDateRangeChange={handleDateRangeChange}
+                  serverTimeZone={serverTimeZone}
                 />
               </div>
               <div className="space-y-1.5 lg:col-span-4">

+ 4 - 2
src/app/[locale]/my-usage/_components/usage-logs-table.tsx

@@ -105,8 +105,7 @@ export function UsageLogsTable({
                     <TooltipProvider>
                       <Tooltip delayDuration={250}>
                         <TooltipTrigger asChild>
-                          <div className="flex items-center justify-end gap-1 cursor-help">
-                            <span>{formatTokenAmount(log.cacheCreationInputTokens)}</span>
+                          <div className="flex items-center gap-2 w-full cursor-help">
                             {log.cacheCreationInputTokens &&
                             log.cacheCreationInputTokens > 0 &&
                             log.cacheTtlApplied ? (
@@ -114,6 +113,9 @@ export function UsageLogsTable({
                                 {log.cacheTtlApplied}
                               </Badge>
                             ) : null}
+                            <span className="ml-auto">
+                              {formatTokenAmount(log.cacheCreationInputTokens)}
+                            </span>
                           </div>
                         </TooltipTrigger>
                         <TooltipContent align="end" className="text-xs space-y-1">

+ 13 - 2
src/app/[locale]/my-usage/page.tsx

@@ -7,6 +7,7 @@ import {
   type MyUsageLogsResult,
   type MyUsageQuota,
 } from "@/actions/my-usage";
+import { getServerTimeZone } from "@/actions/system-config";
 import { useRouter } from "@/i18n/routing";
 import { CollapsibleQuotaCard } from "./_components/collapsible-quota-card";
 import { ExpirationInfo } from "./_components/expiration-info";
@@ -22,6 +23,7 @@ export default function MyUsagePage() {
   const [logsData, setLogsData] = useState<MyUsageLogsResult | null>(null);
   const [isQuotaLoading, setIsQuotaLoading] = useState(true);
   const [isLogsLoading, setIsLogsLoading] = useState(true);
+  const [serverTimeZone, setServerTimeZone] = useState<string | undefined>(undefined);
 
   const loadInitial = useCallback(() => {
     setIsQuotaLoading(true);
@@ -38,6 +40,10 @@ export default function MyUsagePage() {
         if (logsResult.ok) setLogsData(logsResult.data ?? null);
       })
       .finally(() => setIsLogsLoading(false));
+
+    void getServerTimeZone().then((tzResult) => {
+      if (tzResult.ok) setServerTimeZone(tzResult.data.timeZone);
+    });
   }, []);
 
   useEffect(() => {
@@ -76,9 +82,14 @@ export default function MyUsagePage() {
 
       <CollapsibleQuotaCard quota={quota} loading={isQuotaLoading} />
 
-      <StatisticsSummaryCard />
+      <StatisticsSummaryCard serverTimeZone={serverTimeZone} />
 
-      <UsageLogsSection initialData={logsData} loading={isLogsLoading} autoRefreshSeconds={30} />
+      <UsageLogsSection
+        initialData={logsData}
+        loading={isLogsLoading}
+        autoRefreshSeconds={30}
+        serverTimeZone={serverTimeZone}
+      />
     </div>
   );
 }

+ 46 - 14
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx

@@ -6,6 +6,7 @@ import {
   Activity,
   Edit2,
   ExternalLink,
+  InfoIcon,
   Loader2,
   MoreHorizontal,
   Play,
@@ -67,6 +68,7 @@ import {
   TableHeader,
   TableRow,
 } from "@/components/ui/table";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { getProviderTypeConfig, getProviderTypeTranslationKey } from "@/lib/provider-type-utils";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import { getErrorMessage } from "@/lib/utils/error-messages";
@@ -150,7 +152,7 @@ export function ProviderVendorView(props: ProviderVendorViewProps) {
         const vendor = vendors.find((v) => v.id === vendorId);
         const vendorProviders = providersByVendor[vendorId] || [];
 
-        if (!vendor && vendorProviders.length === 0) return null;
+        if (vendorProviders.length === 0) return null;
 
         return (
           <VendorCard
@@ -210,6 +212,21 @@ function VendorCard({
             <div>
               <CardTitle className="flex items-center gap-2">
                 {displayName}
+                {vendorId > 0 && (
+                  <TooltipProvider>
+                    <Tooltip delayDuration={200}>
+                      <TooltipTrigger asChild>
+                        <button
+                          type="button"
+                          className="text-muted-foreground hover:text-foreground"
+                        >
+                          <InfoIcon className="h-3.5 w-3.5" aria-hidden="true" />
+                        </button>
+                      </TooltipTrigger>
+                      <TooltipContent>{t("vendorAggregationRule")}</TooltipContent>
+                    </Tooltip>
+                  </TooltipProvider>
+                )}
                 {websiteUrl && (
                   <a
                     href={websiteUrl}
@@ -427,6 +444,7 @@ function EndpointRow({ endpoint }: { endpoint: ProviderEndpoint }) {
   const tCommon = useTranslations("settings.common");
   const queryClient = useQueryClient();
   const [isProbing, setIsProbing] = useState(false);
+  const [isToggling, setIsToggling] = useState(false);
 
   const probeMutation = useMutation({
     mutationFn: async () => {
@@ -469,11 +487,30 @@ function EndpointRow({ endpoint }: { endpoint: ProviderEndpoint }) {
     },
   });
 
+  const toggleMutation = useMutation({
+    mutationFn: async (nextEnabled: boolean) => {
+      const res = await editProviderEndpoint({
+        endpointId: endpoint.id,
+        isEnabled: nextEnabled,
+      });
+      if (!res.ok) throw new Error(res.error);
+      return res.data;
+    },
+    onMutate: () => setIsToggling(true),
+    onSettled: () => setIsToggling(false),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["provider-endpoints"] });
+      toast.success(t("endpointUpdateSuccess"));
+    },
+    onError: () => {
+      toast.error(t("endpointUpdateFailed"));
+    },
+  });
+
   return (
     <TableRow>
       <TableCell className="font-mono text-xs max-w-[200px] truncate" title={endpoint.url}>
         {endpoint.url}
-        {endpoint.label && <div className="text-muted-foreground font-sans">{endpoint.label}</div>}
       </TableCell>
       <TableCell>
         <div className="flex items-center gap-2">
@@ -487,6 +524,12 @@ function EndpointRow({ endpoint }: { endpoint: ProviderEndpoint }) {
           ) : (
             <Badge variant="outline">{t("disabledStatus")}</Badge>
           )}
+          <Switch
+            checked={endpoint.isEnabled}
+            onCheckedChange={(checked) => toggleMutation.mutate(checked)}
+            disabled={isToggling}
+            aria-label={t("enabledStatus")}
+          />
         </div>
       </TableCell>
       <TableCell>
@@ -568,14 +611,13 @@ function AddEndpointButton({
     setIsSubmitting(true);
     const formData = new FormData(e.currentTarget);
     const endpointUrl = formData.get("url") as string;
-    const label = formData.get("label") as string;
 
     try {
       const res = await addProviderEndpoint({
         vendorId,
         providerType,
         url: endpointUrl,
-        label: label || null,
+        label: null,
         sortOrder: 0,
         isEnabled: true,
       });
@@ -618,10 +660,6 @@ function AddEndpointButton({
               onChange={(e) => setUrl(e.target.value)}
             />
           </div>
-          <div className="space-y-2">
-            <Label htmlFor="label">{t("endpointLabelOptional")}</Label>
-            <Input id="label" name="label" placeholder={t("endpointLabelPlaceholder")} />
-          </div>
 
           <UrlPreview baseUrl={url} providerType={providerType} />
 
@@ -652,14 +690,12 @@ function EditEndpointDialog({ endpoint }: { endpoint: ProviderEndpoint }) {
     setIsSubmitting(true);
     const formData = new FormData(e.currentTarget);
     const url = formData.get("url") as string;
-    const label = formData.get("label") as string;
     const isEnabled = formData.get("isEnabled") === "on";
 
     try {
       const res = await editProviderEndpoint({
         endpointId: endpoint.id,
         url,
-        label: label || null,
         isEnabled,
       });
 
@@ -693,10 +729,6 @@ function EditEndpointDialog({ endpoint }: { endpoint: ProviderEndpoint }) {
             <Label htmlFor="url">{t("endpointUrlLabel")}</Label>
             <Input id="url" name="url" defaultValue={endpoint.url} required />
           </div>
-          <div className="space-y-2">
-            <Label htmlFor="label">{t("endpointLabelOptional")}</Label>
-            <Input id="label" name="label" defaultValue={endpoint.label || ""} />
-          </div>
           <div className="flex items-center space-x-2">
             <Switch id="isEnabled" name="isEnabled" defaultChecked={endpoint.isEnabled} />
             <Label htmlFor="isEnabled">{t("enabledStatus")}</Label>

+ 5 - 1
src/components/customs/active-sessions-list.tsx

@@ -2,9 +2,9 @@
 
 import { useQuery } from "@tanstack/react-query";
 import { Activity, Loader2 } from "lucide-react";
-import { useRouter } from "next/navigation";
 import { useTranslations } from "next-intl";
 import { getActiveSessions } from "@/actions/active-sessions";
+import { useRouter } from "@/i18n/routing";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import type { ActiveSessionInfo } from "@/types/session";
 import { SessionListItem } from "./session-list-item";
@@ -28,6 +28,8 @@ interface ActiveSessionsListProps {
   showHeader?: boolean;
   /** 容器最大高度 */
   maxHeight?: string;
+  /** 是否显示 Token/成本(默认显示) */
+  showTokensCost?: boolean;
   /** 自定义类名 */
   className?: string;
 }
@@ -43,6 +45,7 @@ export function ActiveSessionsList({
   maxItems,
   showHeader = true,
   maxHeight = "200px",
+  showTokensCost = true,
   className = "",
 }: ActiveSessionsListProps) {
   const router = useRouter();
@@ -103,6 +106,7 @@ export function ActiveSessionsList({
                 key={session.sessionId}
                 session={session}
                 currencyCode={currencyCode}
+                showTokensCost={showTokensCost}
               />
             ))}
           </div>

+ 24 - 16
src/components/customs/session-list-item.tsx

@@ -41,13 +41,17 @@ function getStatusIcon(status: "in_progress" | "completed" | "error", statusCode
  * 简洁的 Session 列表项
  * 可复用组件,用于活跃 Session 列表的单项展示
  */
+export interface SessionListItemProps {
+  session: ActiveSessionInfo;
+  currencyCode?: CurrencyCode;
+  showTokensCost?: boolean;
+}
+
 export function SessionListItem({
   session,
   currencyCode = "USD",
-}: {
-  session: ActiveSessionInfo;
-  currencyCode?: CurrencyCode;
-}) {
+  showTokensCost = true,
+}: SessionListItemProps) {
   const statusInfo = getStatusIcon(session.status, session.statusCode);
   const StatusIcon = statusInfo.icon;
   const inputTokensDisplay =
@@ -106,18 +110,22 @@ export function SessionListItem({
         </div>
 
         {/* Token 和成本 */}
-        <div className="flex items-center gap-2 text-xs font-mono flex-shrink-0">
-          {(inputTokensDisplay || outputTokensDisplay) && (
-            <span className="text-muted-foreground">
-              {inputTokensDisplay && `↑${inputTokensDisplay}`}
-              {inputTokensDisplay && outputTokensDisplay && " "}
-              {outputTokensDisplay && `↓${outputTokensDisplay}`}
-            </span>
-          )}
-          {session.costUsd && (
-            <span className="font-medium">{formatCurrency(session.costUsd, currencyCode, 4)}</span>
-          )}
-        </div>
+        {showTokensCost && (
+          <div className="flex items-center gap-2 text-xs font-mono flex-shrink-0">
+            {(inputTokensDisplay || outputTokensDisplay) && (
+              <span className="text-muted-foreground">
+                {inputTokensDisplay && `↑${inputTokensDisplay}`}
+                {inputTokensDisplay && outputTokensDisplay && " "}
+                {outputTokensDisplay && `↓${outputTokensDisplay}`}
+              </span>
+            )}
+            {session.costUsd && (
+              <span className="font-medium">
+                {formatCurrency(session.costUsd, currencyCode, 4)}
+              </span>
+            )}
+          </div>
+        )}
       </div>
     </Link>
   );

+ 123 - 0
src/components/ui/__tests__/tag-input-dialog.test.tsx

@@ -0,0 +1,123 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { useState } from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { describe, expect, test, afterEach } from "vitest";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { TagInput } from "@/components/ui/tag-input";
+
+function render(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(node);
+  });
+
+  return {
+    container,
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+afterEach(() => {
+  while (document.body.firstChild) {
+    document.body.removeChild(document.body.firstChild);
+  }
+});
+
+function DialogTagInput() {
+  const [value, setValue] = useState<string[]>([]);
+
+  return (
+    <Dialog open>
+      <DialogContent>
+        <DialogHeader>
+          <DialogTitle>Tag Input</DialogTitle>
+          <DialogDescription>Tag input dialog test</DialogDescription>
+        </DialogHeader>
+        <TagInput
+          value={value}
+          onChange={setValue}
+          suggestions={[
+            { value: "tag1", label: "Tag 1" },
+            { value: "tag2", label: "Tag 2" },
+          ]}
+        />
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+describe("TagInput inside Dialog", () => {
+  test("renders suggestions under dialog content and supports click selection", async () => {
+    const { container, unmount } = render(<DialogTagInput />);
+
+    const input = document.querySelector("input");
+    expect(input).not.toBeNull();
+
+    await act(async () => {
+      input?.focus();
+      await new Promise((resolve) => setTimeout(resolve, 50));
+    });
+
+    const dialogContent = document.querySelector('[data-slot="dialog-content"]');
+    expect(dialogContent).not.toBeNull();
+    const suggestionButton = Array.from(dialogContent?.querySelectorAll("button") ?? []).find(
+      (button) => button.textContent === "Tag 1"
+    );
+
+    expect(suggestionButton).not.toBeNull();
+    expect(suggestionButton?.closest('[data-slot="dialog-content"]')).not.toBeNull();
+
+    await act(async () => {
+      suggestionButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+    });
+
+    const dialogContentAfterClick = document.querySelector('[data-slot="dialog-content"]');
+    expect(dialogContentAfterClick?.textContent).toContain("tag1");
+
+    unmount();
+  });
+
+  test("supports keyboard selection within dialog", async () => {
+    const { container, unmount } = render(<DialogTagInput />);
+
+    const input = document.querySelector("input");
+    expect(input).not.toBeNull();
+
+    await act(async () => {
+      input?.focus();
+      await new Promise((resolve) => setTimeout(resolve, 50));
+    });
+
+    await act(async () => {
+      input?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
+      await new Promise((resolve) => setTimeout(resolve, 0));
+    });
+
+    await act(async () => {
+      input?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+      await new Promise((resolve) => setTimeout(resolve, 0));
+    });
+
+    const dialogContentAfterKey = document.querySelector('[data-slot="dialog-content"]');
+    expect(dialogContentAfterKey?.textContent).toContain("tag1");
+
+    unmount();
+  });
+});

+ 42 - 22
src/components/ui/tag-input.tsx

@@ -72,6 +72,7 @@ export function TagInput({
     left: number;
     width: number;
   } | null>(null);
+  const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
   const inputRef = React.useRef<HTMLInputElement>(null);
   const containerRef = React.useRef<HTMLDivElement>(null);
   const dropdownRef = React.useRef<HTMLDivElement>(null);
@@ -100,42 +101,58 @@ export function TagInput({
     previousShowSuggestions.current = showSuggestions;
   }, [showSuggestions, onSuggestionsClose]);
 
-  // Calculate dropdown position when showing suggestions
-  // Using fixed positioning, so use viewport coordinates directly (no scroll offset)
-  React.useEffect(() => {
-    if (showSuggestions && containerRef.current) {
-      const rect = containerRef.current.getBoundingClientRect();
-      setDropdownPosition({
-        top: rect.bottom + 4,
-        left: rect.left,
+  React.useLayoutEffect(() => {
+    if (!containerRef.current) return;
+    const dialogContent = containerRef.current.closest('[data-slot="dialog-content"]');
+    setPortalContainer(dialogContent instanceof HTMLElement ? dialogContent : null);
+  }, []);
+
+  const getDropdownPosition = React.useCallback(() => {
+    if (!containerRef.current) return null;
+    const rect = containerRef.current.getBoundingClientRect();
+    if (portalContainer) {
+      const containerRect = portalContainer.getBoundingClientRect();
+      return {
+        top: rect.bottom - containerRect.top + portalContainer.scrollTop + 4,
+        left: rect.left - containerRect.left + portalContainer.scrollLeft,
         width: rect.width,
-      });
+      };
     }
-  }, [showSuggestions]);
+    return {
+      top: rect.bottom + 4,
+      left: rect.left,
+      width: rect.width,
+    };
+  }, [portalContainer]);
+
+  React.useEffect(() => {
+    if (!showSuggestions) return;
+    const position = getDropdownPosition();
+    if (position) {
+      setDropdownPosition(position);
+    }
+  }, [showSuggestions, getDropdownPosition]);
 
   // Update position on scroll/resize (recalculate viewport coords)
   React.useEffect(() => {
     if (!showSuggestions) return;
 
     const updatePosition = () => {
-      if (containerRef.current) {
-        const rect = containerRef.current.getBoundingClientRect();
-        setDropdownPosition({
-          top: rect.bottom + 4,
-          left: rect.left,
-          width: rect.width,
-        });
+      const position = getDropdownPosition();
+      if (position) {
+        setDropdownPosition(position);
       }
     };
 
-    window.addEventListener("scroll", updatePosition, true);
+    const scrollTarget: HTMLElement | Window = portalContainer ?? window;
+    scrollTarget.addEventListener("scroll", updatePosition, true);
     window.addEventListener("resize", updatePosition);
 
     return () => {
-      window.removeEventListener("scroll", updatePosition, true);
+      scrollTarget.removeEventListener("scroll", updatePosition, true);
       window.removeEventListener("resize", updatePosition);
     };
-  }, [showSuggestions]);
+  }, [showSuggestions, getDropdownPosition, portalContainer]);
 
   // Close dropdown when clicking outside
   React.useEffect(() => {
@@ -470,10 +487,13 @@ export function TagInput({
       )}
       {/* 建议下拉列表 - 使用 Radix Portal 确保在 Dialog 中正确渲染 */}
       {showSuggestions && filteredSuggestions.length > 0 && dropdownPosition && (
-        <Portal.Root>
+        <Portal.Root container={portalContainer ?? undefined}>
           <div
             ref={dropdownRef}
-            className="fixed z-[9999] rounded-md border bg-popover shadow-md max-h-48 overflow-auto"
+            className={cn(
+              portalContainer ? "absolute" : "fixed",
+              "z-[9999] rounded-md border bg-popover shadow-md max-h-48 overflow-auto"
+            )}
             style={{
               top: dropdownPosition.top,
               left: dropdownPosition.left,

+ 12 - 9
src/drizzle/schema.ts

@@ -6,6 +6,7 @@ import {
   timestamp,
   boolean,
   integer,
+  bigint,
   numeric,
   jsonb,
   index,
@@ -151,9 +152,11 @@ export const providers = pgTable('providers', {
   description: text('description'),
   url: varchar('url').notNull(),
   key: varchar('key').notNull(),
-  providerVendorId: integer('provider_vendor_id').references(() => providerVendors.id, {
-    onDelete: 'restrict',
-  }),
+  providerVendorId: integer('provider_vendor_id')
+    .notNull()
+    .references(() => providerVendors.id, {
+      onDelete: 'restrict',
+    }),
   isEnabled: boolean('is_enabled').notNull().default(true),
   weight: integer('weight').notNull().default(1),
 
@@ -397,13 +400,13 @@ export const messageRequest = pgTable('message_request', {
   originalModel: varchar('original_model', { length: 128 }),
 
   // Token 使用信息
-  inputTokens: integer('input_tokens'),
-  outputTokens: integer('output_tokens'),
+  inputTokens: bigint('input_tokens', { mode: 'number' }),
+  outputTokens: bigint('output_tokens', { mode: 'number' }),
   ttfbMs: integer('ttfb_ms'),
-  cacheCreationInputTokens: integer('cache_creation_input_tokens'),
-  cacheReadInputTokens: integer('cache_read_input_tokens'),
-  cacheCreation5mInputTokens: integer('cache_creation_5m_input_tokens'),
-  cacheCreation1hInputTokens: integer('cache_creation_1h_input_tokens'),
+  cacheCreationInputTokens: bigint('cache_creation_input_tokens', { mode: 'number' }),
+  cacheReadInputTokens: bigint('cache_read_input_tokens', { mode: 'number' }),
+  cacheCreation5mInputTokens: bigint('cache_creation_5m_input_tokens', { mode: 'number' }),
+  cacheCreation1hInputTokens: bigint('cache_creation_1h_input_tokens', { mode: 'number' }),
   cacheTtlApplied: varchar('cache_ttl_applied', { length: 10 }),
 
   // 1M Context Window 应用状态

+ 13 - 13
src/repository/key.ts

@@ -335,11 +335,11 @@ export async function findKeyUsageTodayBatch(
       keyId: keys.id,
       totalCost: sum(messageRequest.costUsd),
       totalTokens: sql<number>`COALESCE(SUM(
-        COALESCE(${messageRequest.inputTokens}, 0) +
-        COALESCE(${messageRequest.outputTokens}, 0) +
-        COALESCE(${messageRequest.cacheCreationInputTokens}, 0) +
-        COALESCE(${messageRequest.cacheReadInputTokens}, 0)
-      ), 0)::int`,
+        COALESCE(${messageRequest.inputTokens}, 0)::double precision +
+        COALESCE(${messageRequest.outputTokens}, 0)::double precision +
+        COALESCE(${messageRequest.cacheCreationInputTokens}, 0)::double precision +
+        COALESCE(${messageRequest.cacheReadInputTokens}, 0)::double precision
+      ), 0::double precision)`,
     })
     .from(keys)
     .leftJoin(
@@ -622,10 +622,10 @@ export async function findKeysWithStatistics(userId: number): Promise<KeyStatist
         model: messageRequest.model,
         callCount: sql<number>`count(*)::int`,
         totalCost: sum(messageRequest.costUsd),
-        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
-        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`,
-        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`,
+        inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+        outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
+        cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::double precision`,
+        cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::double precision`,
       })
       .from(messageRequest)
       .where(
@@ -771,10 +771,10 @@ export async function findKeysWithStatisticsBatch(
       model: messageRequest.model,
       callCount: sql<number>`count(*)::int`,
       totalCost: sum(messageRequest.costUsd),
-      inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`,
-      outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`,
-      cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`,
-      cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`,
+      inputTokens: sql<number>`COALESCE(sum(${messageRequest.inputTokens}), 0)::double precision`,
+      outputTokens: sql<number>`COALESCE(sum(${messageRequest.outputTokens}), 0)::double precision`,
+      cacheCreationTokens: sql<number>`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::double precision`,
+      cacheReadTokens: sql<number>`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::double precision`,
     })
     .from(messageRequest)
     .where(

+ 3 - 3
src/repository/leaderboard.ts

@@ -439,9 +439,9 @@ async function findProviderCacheHitRateLeaderboardWithTimezone(
   providerType?: ProviderType
 ): Promise<ProviderCacheHitRateLeaderboardEntry[]> {
   const totalInputTokensExpr = sql<number>`(
-    COALESCE(${messageRequest.inputTokens}, 0) +
-    COALESCE(${messageRequest.cacheCreationInputTokens}, 0) +
-    COALESCE(${messageRequest.cacheReadInputTokens}, 0)
+    COALESCE(${messageRequest.inputTokens}, 0)::double precision +
+    COALESCE(${messageRequest.cacheCreationInputTokens}, 0)::double precision +
+    COALESCE(${messageRequest.cacheReadInputTokens}, 0)::double precision
   )`;
 
   const cacheRequiredCondition = sql`(

+ 20 - 4
src/repository/provider-endpoints.ts

@@ -231,9 +231,25 @@ export async function getOrCreateProviderVendorIdFromUrls(input: {
  * 从域名派生显示名称(直接使用域名的中间部分)
  * 例如: anthropic.com -> Anthropic, api.openai.com -> OpenAI
  */
-function deriveDisplayNameFromDomain(domain: string): string {
-  const parts = domain.split(".");
-  const name = parts[0] === "api" && parts[1] ? parts[1] : parts[0];
+export async function deriveDisplayNameFromDomain(domain: string): Promise<string> {
+  const parts = domain
+    .split(".")
+    .map((part) => part.trim())
+    .filter(Boolean);
+  if (parts.length === 0) return "";
+  if (parts.length === 1) {
+    const name = parts[0];
+    return name.charAt(0).toUpperCase() + name.slice(1);
+  }
+
+  const apiPrefixes = new Set(["api", "v1", "v2", "v3", "www"]);
+  let name = parts[parts.length - 2];
+  if (apiPrefixes.has(name) && parts.length >= 3) {
+    name = parts[parts.length - 3];
+  }
+  if (apiPrefixes.has(name) && parts.length >= 4) {
+    name = parts[parts.length - 4];
+  }
   return name.charAt(0).toUpperCase() + name.slice(1);
 }
 
@@ -299,7 +315,7 @@ export async function backfillProviderVendorsFromProviders(): Promise<{
       }
 
       try {
-        const displayName = deriveDisplayNameFromDomain(domain);
+        const displayName = await deriveDisplayNameFromDomain(domain);
         const vendorId = await getOrCreateProviderVendorIdFromUrls({
           providerUrl: row.url,
           websiteUrl: row.websiteUrl ?? null,

+ 208 - 0
tests/unit/actions/my-usage-token-aggregation.test.ts

@@ -0,0 +1,208 @@
+import { describe, expect, test, vi } from "vitest";
+
+// 禁用 tests/setup.ts 中基于 DSN/Redis 的默认同步与清理协调,避免无关依赖引入。
+process.env.DSN = "";
+process.env.AUTO_CLEANUP_TEST_DATA = "false";
+
+function sqlToString(sqlObj: unknown): string {
+  const visited = new Set<unknown>();
+
+  const walk = (node: unknown): string => {
+    if (!node || visited.has(node)) return "";
+    visited.add(node);
+
+    if (typeof node === "string") return node;
+
+    if (typeof node === "object") {
+      const anyNode = node as any;
+      if (Array.isArray(anyNode)) {
+        return anyNode.map(walk).join("");
+      }
+
+      if (anyNode.value) {
+        if (Array.isArray(anyNode.value)) {
+          return anyNode.value.map(String).join("");
+        }
+        return String(anyNode.value);
+      }
+
+      if (anyNode.queryChunks) {
+        return walk(anyNode.queryChunks);
+      }
+    }
+
+    return "";
+  };
+
+  return walk(sqlObj);
+}
+
+function createThenableQuery<T>(result: T) {
+  const query: any = Promise.resolve(result);
+
+  query.from = vi.fn(() => query);
+  query.innerJoin = vi.fn(() => query);
+  query.leftJoin = vi.fn(() => query);
+  query.where = vi.fn(() => query);
+  query.groupBy = vi.fn(() => query);
+  query.orderBy = vi.fn(() => query);
+  query.limit = vi.fn(() => query);
+  query.offset = vi.fn(() => query);
+
+  return query;
+}
+
+const mocks = vi.hoisted(() => ({
+  getSession: vi.fn(),
+  getSystemSettings: vi.fn(),
+  getEnvConfig: vi.fn(),
+  getTimeRangeForPeriodWithMode: vi.fn(),
+  findUsageLogsStats: vi.fn(),
+  select: vi.fn(),
+  execute: vi.fn(async () => ({ count: 0 })),
+}));
+
+vi.mock("@/lib/auth", () => ({
+  getSession: mocks.getSession,
+}));
+
+vi.mock("@/repository/system-config", () => ({
+  getSystemSettings: mocks.getSystemSettings,
+}));
+
+vi.mock("@/lib/config", () => ({
+  getEnvConfig: mocks.getEnvConfig,
+}));
+
+vi.mock("@/lib/rate-limit/time-utils", () => ({
+  getTimeRangeForPeriodWithMode: mocks.getTimeRangeForPeriodWithMode,
+}));
+
+vi.mock("@/repository/usage-logs", async (importOriginal) => {
+  const actual = await importOriginal<typeof import("@/repository/usage-logs")>();
+  return {
+    ...actual,
+    findUsageLogsStats: mocks.findUsageLogsStats,
+  };
+});
+
+vi.mock("@/drizzle/db", () => ({
+  db: {
+    select: mocks.select,
+    execute: mocks.execute,
+  },
+}));
+
+function expectNoIntTokenSum(selection: Record<string, unknown>, field: string) {
+  const tokenSql = sqlToString(selection[field]).toLowerCase();
+  expect(tokenSql).toContain("sum");
+  expect(tokenSql).not.toContain("::int");
+  expect(tokenSql).not.toContain("::int4");
+  expect(tokenSql).toContain("double precision");
+}
+
+describe("my-usage token aggregation", () => {
+  test("getMyTodayStats: token sum 不应使用 ::int", async () => {
+    vi.resetModules();
+
+    const capturedSelections: Array<Record<string, unknown>> = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(
+      createThenableQuery([
+        {
+          calls: 0,
+          inputTokens: 0,
+          outputTokens: 0,
+          costUsd: "0",
+        },
+      ])
+    );
+    selectQueue.push(createThenableQuery([]));
+
+    mocks.select.mockImplementation((selection: unknown) => {
+      capturedSelections.push(selection as Record<string, unknown>);
+      return selectQueue.shift() ?? createThenableQuery([]);
+    });
+
+    mocks.getTimeRangeForPeriodWithMode.mockReturnValue({
+      startTime: new Date("2024-01-01T00:00:00.000Z"),
+      endTime: new Date("2024-01-02T00:00:00.000Z"),
+    });
+
+    mocks.getSession.mockResolvedValue({
+      key: {
+        id: 1,
+        key: "k",
+        dailyResetTime: "00:00",
+        dailyResetMode: "fixed",
+      },
+      user: { id: 1 },
+    });
+
+    mocks.getSystemSettings.mockResolvedValue({
+      currencyDisplay: "USD",
+      billingModelSource: "original",
+    });
+
+    const { getMyTodayStats } = await import("@/actions/my-usage");
+    const res = await getMyTodayStats();
+    expect(res.ok).toBe(true);
+
+    expect(capturedSelections.length).toBeGreaterThanOrEqual(2);
+    expectNoIntTokenSum(capturedSelections[0], "inputTokens");
+    expectNoIntTokenSum(capturedSelections[0], "outputTokens");
+    expectNoIntTokenSum(capturedSelections[1], "inputTokens");
+    expectNoIntTokenSum(capturedSelections[1], "outputTokens");
+  });
+
+  test("getMyStatsSummary: token sum 不应使用 ::int", async () => {
+    vi.resetModules();
+
+    const capturedSelections: Array<Record<string, unknown>> = [];
+    const selectQueue: any[] = [];
+    selectQueue.push(createThenableQuery([]));
+    selectQueue.push(createThenableQuery([]));
+
+    mocks.select.mockImplementation((selection: unknown) => {
+      capturedSelections.push(selection as Record<string, unknown>);
+      return selectQueue.shift() ?? createThenableQuery([]);
+    });
+
+    mocks.getEnvConfig.mockReturnValue({ TZ: "UTC" });
+
+    mocks.getSession.mockResolvedValue({
+      key: { id: 1, key: "k" },
+      user: { id: 1 },
+    });
+
+    mocks.getSystemSettings.mockResolvedValue({
+      currencyDisplay: "USD",
+      billingModelSource: "original",
+    });
+
+    mocks.findUsageLogsStats.mockResolvedValue({
+      totalRequests: 0,
+      totalCost: 0,
+      totalTokens: 0,
+      totalInputTokens: 0,
+      totalOutputTokens: 0,
+      totalCacheCreationTokens: 0,
+      totalCacheReadTokens: 0,
+      totalCacheCreation5mTokens: 0,
+      totalCacheCreation1hTokens: 0,
+    });
+
+    const { getMyStatsSummary } = await import("@/actions/my-usage");
+    const res = await getMyStatsSummary({ startDate: "2024-01-01", endDate: "2024-01-01" });
+    expect(res.ok).toBe(true);
+
+    expect(capturedSelections).toHaveLength(2);
+
+    for (const selection of capturedSelections) {
+      expectNoIntTokenSum(selection, "inputTokens");
+      expectNoIntTokenSum(selection, "outputTokens");
+      expectNoIntTokenSum(selection, "cacheCreationTokens");
+      expectNoIntTokenSum(selection, "cacheReadTokens");
+    }
+  });
+});

+ 104 - 0
tests/unit/components/session-list-item.test.tsx

@@ -0,0 +1,104 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, test, vi } from "vitest";
+import { SessionListItem } from "@/components/customs/session-list-item";
+import type { CurrencyCode } from "@/lib/utils/currency";
+import type { ActiveSessionInfo } from "@/types/session";
+
+vi.mock("@/i18n/routing", () => ({
+  Link: ({
+    href,
+    children,
+    ...rest
+  }: {
+    href: string;
+    children: ReactNode;
+    className?: string;
+  }) => (
+    <a href={href} {...rest}>
+      {children}
+    </a>
+  ),
+}));
+
+vi.mock("@/lib/utils/currency", async () => {
+  const actual =
+    await vi.importActual<typeof import("@/lib/utils/currency")>("@/lib/utils/currency");
+  return {
+    ...actual,
+    formatCurrency: () => "__COST__",
+  };
+});
+
+const UP_ARROW = "\u2191";
+const DOWN_ARROW = "\u2193";
+const COST_SENTINEL = "__COST__";
+
+type SessionListItemProps = {
+  session: ActiveSessionInfo;
+  currencyCode?: CurrencyCode;
+  showTokensCost?: boolean;
+};
+
+const SessionListItemTest = SessionListItem as unknown as (
+  props: SessionListItemProps
+) => JSX.Element;
+
+const baseSession: ActiveSessionInfo = {
+  sessionId: "session-1",
+  userName: "alice",
+  userId: 1,
+  keyId: 2,
+  keyName: "key-1",
+  providerId: 3,
+  providerName: "openai",
+  model: "gpt-4.1",
+  apiType: "chat",
+  startTime: 1700000000000,
+  status: "completed",
+  durationMs: 1500,
+  inputTokens: 100,
+  outputTokens: 50,
+  costUsd: "0.0123",
+};
+
+function renderTextContent(options?: {
+  showTokensCost?: boolean;
+  sessionOverrides?: Partial<ActiveSessionInfo>;
+}) {
+  const session = { ...baseSession, ...(options?.sessionOverrides ?? {}) };
+  const html = renderToStaticMarkup(
+    <SessionListItemTest session={session} showTokensCost={options?.showTokensCost} />
+  );
+  const container = document.createElement("div");
+  container.innerHTML = html;
+  return container.textContent ?? "";
+}
+
+describe("SessionListItem showTokensCost", () => {
+  test("hides tokens and cost when disabled but keeps core fields", () => {
+    const text = renderTextContent({ showTokensCost: false });
+
+    expect(text).not.toContain(`${UP_ARROW}100`);
+    expect(text).not.toContain(`${DOWN_ARROW}50`);
+    expect(text).not.toContain(COST_SENTINEL);
+
+    expect(text).toContain("alice");
+    expect(text).toContain("key-1");
+    expect(text).toContain("gpt-4.1");
+    expect(text).toContain("@ openai");
+    expect(text).toContain("1.5s");
+  });
+
+  test("shows tokens and cost by default", () => {
+    const text = renderTextContent();
+
+    expect(text).toContain(`${UP_ARROW}100`);
+    expect(text).toContain(`${DOWN_ARROW}50`);
+    expect(text).toContain(COST_SENTINEL);
+  });
+});

+ 15 - 0
tests/unit/dashboard-logs-time-range-utils.test.ts

@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
 import {
   dateStringWithClockToTimestamp,
   formatClockFromTimestamp,
+  getQuickDateRange,
   inclusiveEndTimestampFromExclusive,
   parseClockString,
 } from "@/app/[locale]/dashboard/logs/_utils/time-range";
@@ -43,4 +44,18 @@ describe("dashboard logs time range utils", () => {
     const ts = new Date(2026, 0, 1, 1, 2, 3, 0).getTime();
     expect(formatClockFromTimestamp(ts)).toBe("01:02:03");
   });
+
+  test("getQuickDateRange uses server timezone for today/yesterday", () => {
+    const now = new Date("2024-01-02T02:00:00Z");
+    const tz = "America/Los_Angeles";
+
+    expect(getQuickDateRange("today", tz, now)).toEqual({
+      startDate: "2024-01-01",
+      endDate: "2024-01-01",
+    });
+    expect(getQuickDateRange("yesterday", tz, now)).toEqual({
+      startDate: "2023-12-31",
+      endDate: "2023-12-31",
+    });
+  });
 });

+ 20 - 4
tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx

@@ -41,11 +41,11 @@ vi.mock("@/actions/usage-logs", () => ({
           statusCode: 200,
           inputTokens: 1,
           outputTokens: 1,
-          cacheCreationInputTokens: 0,
-          cacheReadInputTokens: 0,
-          cacheCreation5mInputTokens: 0,
+          cacheCreationInputTokens: 10,
+          cacheReadInputTokens: 5,
+          cacheCreation5mInputTokens: 10,
           cacheCreation1hInputTokens: 0,
-          cacheTtlApplied: null,
+          cacheTtlApplied: "1h",
           totalTokens: 2,
           costUsd: "0.000001",
           costMultiplier: null,
@@ -160,3 +160,19 @@ describe("VirtualizedLogsTable - specialSettings display", () => {
     unmount();
   });
 });
+
+describe("VirtualizedLogsTable - cache badge alignment", () => {
+  test("badge renders left while numbers stay right", async () => {
+    const { container, unmount } = renderWithIntl(
+      <VirtualizedLogsTable filters={{}} autoRefreshEnabled={false} />
+    );
+
+    await flushMicrotasks();
+    await waitForText(container, "Loaded 1 records");
+
+    expect(container.innerHTML).toContain("1h");
+    expect(container.innerHTML).toContain("ml-auto");
+
+    unmount();
+  });
+});

+ 53 - 0
tests/unit/dashboard-logs-warmup-ui.test.tsx

@@ -102,3 +102,56 @@ describe("UsageLogsTable - warmup 跳过展示", () => {
     unmount();
   });
 });
+
+describe("UsageLogsTable - cache badge alignment", () => {
+  test("badge renders before numbers and keeps right-aligned tokens", () => {
+    const cacheLog: UsageLogRow = {
+      id: 2,
+      createdAt: new Date(),
+      sessionId: "session_cache",
+      requestSequence: 1,
+      userName: "user",
+      keyName: "key",
+      providerName: "provider",
+      model: "claude-sonnet-4-5-20250929",
+      originalModel: "claude-sonnet-4-5-20250929",
+      endpoint: "/v1/messages",
+      statusCode: 200,
+      inputTokens: 10,
+      outputTokens: 5,
+      cacheCreationInputTokens: 10,
+      cacheReadInputTokens: 5,
+      cacheCreation5mInputTokens: 10,
+      cacheCreation1hInputTokens: 0,
+      cacheTtlApplied: "1h",
+      totalTokens: 15,
+      costUsd: "0.000001",
+      costMultiplier: null,
+      durationMs: 10,
+      ttfbMs: 5,
+      errorMessage: null,
+      providerChain: null,
+      blockedBy: null,
+      blockedReason: null,
+      userAgent: "claude_cli/1.0",
+      messagesCount: 1,
+      context1mApplied: false,
+    };
+
+    const { container, unmount } = renderWithIntl(
+      <UsageLogsTable
+        logs={[cacheLog]}
+        total={1}
+        page={1}
+        pageSize={50}
+        onPageChange={() => {}}
+        isPending={false}
+      />
+    );
+
+    expect(container.innerHTML).toContain("1h");
+    expect(container.innerHTML).toContain("ml-auto");
+
+    unmount();
+  });
+});

+ 77 - 0
tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx

@@ -0,0 +1,77 @@
+/**
+ * @vitest-environment happy-dom
+ */
+
+import type { ReactNode } from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { NextIntlClientProvider } from "next-intl";
+import { describe, expect, test, vi } from "vitest";
+import { AvailabilityDashboard } from "@/app/[locale]/dashboard/availability/_components/availability-dashboard";
+
+vi.mock("@/app/[locale]/dashboard/availability/_components/overview/overview-section", () => ({
+  OverviewSection: () => <div data-testid="overview-section" />,
+}));
+vi.mock("@/app/[locale]/dashboard/availability/_components/provider/provider-tab", () => ({
+  ProviderTab: () => <div data-testid="provider-tab" />,
+}));
+vi.mock("@/app/[locale]/dashboard/availability/_components/endpoint/endpoint-tab", () => ({
+  EndpointTab: () => <div data-testid="endpoint-tab" />,
+}));
+
+function renderWithIntl(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(
+      <NextIntlClientProvider
+        locale="en"
+        timeZone="UTC"
+        messages={{
+          dashboard: {
+            availability: {
+              tabs: { provider: "Provider", endpoint: "Endpoint" },
+              states: { fetchFailed: "Fetch failed" },
+              actions: {
+                probeAll: "Probe All",
+                probing: "Probing",
+                probeSuccess: "Probe success",
+                probeFailed: "Probe failed",
+              },
+            },
+          },
+        }}
+      >
+        {node}
+      </NextIntlClientProvider>
+    );
+  });
+
+  return {
+    container,
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+describe("AvailabilityDashboard UI", () => {
+  test("does not render Probe All floating button", async () => {
+    vi.stubGlobal(
+      "fetch",
+      vi.fn(async () => ({
+        ok: true,
+        json: async () => ({ providers: [], systemAvailability: 0 }),
+      }))
+    );
+
+    const { container, unmount } = renderWithIntl(<AvailabilityDashboard />);
+
+    expect(container.textContent).not.toContain("Probe All");
+
+    unmount();
+  });
+});

+ 228 - 0
tests/unit/dashboard/dashboard-home-layout.test.tsx

@@ -0,0 +1,228 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import fs from "node:fs";
+import path from "node:path";
+import type { ReactNode } from "react";
+import { act } from "react";
+import { createRoot } from "react-dom/client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { NextIntlClientProvider } from "next-intl";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { DashboardBento } from "@/app/[locale]/dashboard/_components/bento/dashboard-bento";
+import { DashboardMain } from "@/app/[locale]/dashboard/_components/dashboard-main";
+import type { OverviewData } from "@/actions/overview";
+import type { UserStatisticsData } from "@/types/statistics";
+
+const routingMocks = vi.hoisted(() => ({
+  usePathname: vi.fn(),
+}));
+vi.mock("@/i18n/routing", () => ({
+  usePathname: routingMocks.usePathname,
+}));
+
+const overviewMocks = vi.hoisted(() => ({
+  getOverviewData: vi.fn(),
+}));
+vi.mock("@/actions/overview", () => overviewMocks);
+
+const activeSessionsMocks = vi.hoisted(() => ({
+  getActiveSessions: vi.fn(),
+}));
+vi.mock("@/actions/active-sessions", () => activeSessionsMocks);
+
+const statisticsMocks = vi.hoisted(() => ({
+  getUserStatistics: vi.fn(),
+}));
+vi.mock("@/actions/statistics", () => statisticsMocks);
+
+vi.mock("@/app/[locale]/dashboard/_components/bento/live-sessions-panel", () => ({
+  LiveSessionsPanel: () => <div data-testid="live-sessions-panel" />,
+}));
+
+vi.mock("@/app/[locale]/dashboard/_components/bento/leaderboard-card", () => ({
+  LeaderboardCard: () => <div data-testid="leaderboard-card" />,
+}));
+
+vi.mock("@/app/[locale]/dashboard/_components/bento/statistics-chart-card", () => ({
+  StatisticsChartCard: () => <div data-testid="statistics-chart-card" />,
+}));
+
+const customsMessages = JSON.parse(
+  fs.readFileSync(path.join(process.cwd(), "messages/en/customs.json"), "utf8")
+);
+const dashboardMessages = JSON.parse(
+  fs.readFileSync(path.join(process.cwd(), "messages/en/dashboard.json"), "utf8")
+);
+
+const mockOverviewData: OverviewData = {
+  concurrentSessions: 2,
+  todayRequests: 12,
+  todayCost: 1.23,
+  avgResponseTime: 456,
+  todayErrorRate: 0.1,
+  yesterdaySamePeriodRequests: 10,
+  yesterdaySamePeriodCost: 1.01,
+  yesterdaySamePeriodAvgResponseTime: 500,
+  recentMinuteRequests: 3,
+};
+
+const mockStatisticsData: UserStatisticsData = {
+  chartData: [],
+  users: [],
+  timeRange: "today",
+  resolution: "hour",
+  mode: "users",
+};
+
+function renderSimple(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+
+  act(() => {
+    root.render(node);
+  });
+
+  return {
+    container,
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+function renderWithProviders(node: ReactNode) {
+  const container = document.createElement("div");
+  document.body.appendChild(container);
+  const root = createRoot(container);
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        refetchOnWindowFocus: false,
+      },
+    },
+  });
+
+  act(() => {
+    root.render(
+      <QueryClientProvider client={queryClient}>
+        <NextIntlClientProvider
+          locale="en"
+          messages={{ customs: customsMessages, dashboard: dashboardMessages }}
+          timeZone="UTC"
+        >
+          {node}
+        </NextIntlClientProvider>
+      </QueryClientProvider>
+    );
+  });
+
+  return {
+    container,
+    unmount: () => {
+      act(() => root.unmount());
+      container.remove();
+    },
+  };
+}
+
+function findByClassToken(root: ParentNode, token: string) {
+  return Array.from(root.querySelectorAll<HTMLElement>("*")).find((el) =>
+    el.classList.contains(token)
+  );
+}
+
+function findClosestWithClasses(element: Element | null, classes: string[]) {
+  let current = element?.parentElement ?? null;
+  while (current) {
+    const hasAll = classes.every((cls) => current.classList.contains(cls));
+    if (hasAll) return current;
+    current = current.parentElement;
+  }
+  return null;
+}
+
+async function flushPromises() {
+  await act(async () => {
+    await Promise.resolve();
+  });
+}
+
+beforeEach(() => {
+  vi.clearAllMocks();
+  document.body.innerHTML = "";
+  overviewMocks.getOverviewData.mockResolvedValue({ ok: true, data: mockOverviewData });
+  activeSessionsMocks.getActiveSessions.mockResolvedValue({ ok: true, data: [] });
+  statisticsMocks.getUserStatistics.mockResolvedValue({ ok: true, data: mockStatisticsData });
+  vi.stubGlobal(
+    "fetch",
+    vi.fn(async () => ({
+      ok: true,
+      json: async () => [],
+    }))
+  );
+});
+
+afterEach(() => {
+  vi.unstubAllGlobals();
+});
+
+describe("DashboardMain layout classes", () => {
+  test("pathname /dashboard has max-w-7xl and px-6", () => {
+    routingMocks.usePathname.mockReturnValue("/dashboard");
+    const { container, unmount } = renderSimple(
+      <DashboardMain>
+        <div data-testid="content" />
+      </DashboardMain>
+    );
+
+    const main = container.querySelector("main");
+    expect(main).toBeTruthy();
+    expect(main?.className).toContain("px-6");
+    expect(main?.className).toContain("max-w-7xl");
+
+    unmount();
+  });
+
+  test("pathname /dashboard/logs keeps max-w-7xl", () => {
+    routingMocks.usePathname.mockReturnValue("/dashboard/logs");
+    const { container, unmount } = renderSimple(
+      <DashboardMain>
+        <div data-testid="content" />
+      </DashboardMain>
+    );
+
+    const main = container.querySelector("main");
+    expect(main).toBeTruthy();
+    expect(main?.className).toContain("max-w-7xl");
+
+    unmount();
+  });
+});
+
+describe("DashboardBento admin layout", () => {
+  test("renders four-column layout with LiveSessionsPanel in last column", async () => {
+    const { container, unmount } = renderWithProviders(
+      <DashboardBento
+        isAdmin={true}
+        currencyCode="USD"
+        allowGlobalUsageView={false}
+        initialStatistics={mockStatisticsData}
+      />
+    );
+    await flushPromises();
+
+    const grid = findByClassToken(container, "lg:grid-cols-[1fr_1fr_1fr_280px]");
+    expect(grid).toBeTruthy();
+
+    const livePanel = container.querySelector('[data-testid="live-sessions-panel"]');
+    expect(livePanel).toBeTruthy();
+
+    expect(grid?.contains(livePanel as HTMLElement)).toBe(true);
+
+    unmount();
+  });
+});

+ 152 - 0
tests/unit/repository/key-usage-token-overflow.test.ts

@@ -0,0 +1,152 @@
+import { describe, expect, test, vi } from "vitest";
+
+// 禁用 tests/setup.ts 中基于 DSN/Redis 的默认同步与清理协调,避免无关依赖引入。
+process.env.DSN = "";
+process.env.AUTO_CLEANUP_TEST_DATA = "false";
+
+function sqlToString(sqlObj: unknown): string {
+  const visited = new Set<unknown>();
+
+  const walk = (node: unknown): string => {
+    if (!node || visited.has(node)) return "";
+    visited.add(node);
+
+    if (typeof node === "string") return node;
+
+    if (typeof node === "object") {
+      const anyNode = node as any;
+      if (Array.isArray(anyNode)) {
+        return anyNode.map(walk).join("");
+      }
+
+      if (anyNode.value) {
+        if (Array.isArray(anyNode.value)) {
+          return anyNode.value.map(String).join("");
+        }
+        return String(anyNode.value);
+      }
+
+      if (anyNode.queryChunks) {
+        return walk(anyNode.queryChunks);
+      }
+    }
+
+    return "";
+  };
+
+  return walk(sqlObj);
+}
+
+function createThenableQuery<T>(result: T) {
+  const query: any = Promise.resolve(result);
+
+  query.from = vi.fn(() => query);
+  query.leftJoin = vi.fn(() => query);
+  query.innerJoin = vi.fn(() => query);
+  query.where = vi.fn(() => query);
+  query.groupBy = vi.fn(() => query);
+  query.orderBy = vi.fn(() => query);
+  query.limit = vi.fn(() => query);
+  query.offset = vi.fn(() => query);
+
+  return query;
+}
+
+describe("Key usage token aggregation overflow", () => {
+  test("findKeyUsageTodayBatch: token sum 不应使用 ::int", async () => {
+    vi.resetModules();
+
+    const selectArgs: unknown[] = [];
+    const selectMock = vi.fn((selection: unknown) => {
+      selectArgs.push(selection);
+      return createThenableQuery([]);
+    });
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        // 给 tests/setup.ts 的 afterAll 清理逻辑一个可用的 execute
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findKeyUsageTodayBatch } = await import("@/repository/key");
+    await findKeyUsageTodayBatch([1]);
+
+    expect(selectArgs).toHaveLength(1);
+    const selection = selectArgs[0] as Record<string, unknown>;
+    const totalTokensSql = sqlToString(selection.totalTokens).toLowerCase();
+
+    expect(totalTokensSql).not.toContain("::int");
+    expect(totalTokensSql).not.toContain("::int4");
+    expect(totalTokensSql).toContain("double precision");
+  });
+
+  test("findKeysWithStatisticsBatch: modelStats token sum 不应使用 ::int", async () => {
+    vi.resetModules();
+
+    const selectArgs: unknown[] = [];
+    const selectQueue: any[] = [];
+
+    selectQueue.push(
+      createThenableQuery([
+        {
+          id: 10,
+          userId: 1,
+          key: "k",
+          name: "n",
+          isEnabled: true,
+          expiresAt: null,
+          canLoginWebUi: true,
+          limit5hUsd: null,
+          limitDailyUsd: null,
+          dailyResetMode: "fixed",
+          dailyResetTime: "00:00",
+          limitWeeklyUsd: null,
+          limitMonthlyUsd: null,
+          limitTotalUsd: null,
+          limitConcurrentSessions: 0,
+          providerGroup: null,
+          cacheTtlPreference: null,
+          createdAt: new Date("2024-01-01T00:00:00.000Z"),
+          updatedAt: new Date("2024-01-01T00:00:00.000Z"),
+          deletedAt: null,
+        },
+      ])
+    );
+    selectQueue.push(createThenableQuery([]));
+    selectQueue.push(createThenableQuery([]));
+
+    const fallbackSelect = createThenableQuery<unknown[]>([]);
+    const selectMock = vi.fn((selection: unknown) => {
+      selectArgs.push(selection);
+      return selectQueue.shift() ?? fallbackSelect;
+    });
+
+    const selectDistinctOnMock = vi.fn(() => createThenableQuery([]));
+
+    vi.doMock("@/drizzle/db", () => ({
+      db: {
+        select: selectMock,
+        selectDistinctOn: selectDistinctOnMock,
+        execute: vi.fn(async () => ({ count: 0 })),
+      },
+    }));
+
+    const { findKeysWithStatisticsBatch } = await import("@/repository/key");
+    await findKeysWithStatisticsBatch([1]);
+
+    const selection = selectArgs.find((s): s is Record<string, unknown> => {
+      if (!s || typeof s !== "object") return false;
+      return "inputTokens" in s && "cacheReadTokens" in s;
+    });
+    expect(selection).toBeTruthy();
+
+    for (const field of ["inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens"]) {
+      const tokenSql = sqlToString(selection?.[field]).toLowerCase();
+      expect(tokenSql).not.toContain("::int");
+      expect(tokenSql).not.toContain("::int4");
+      expect(tokenSql).toContain("double precision");
+    }
+  });
+});

+ 27 - 0
tests/unit/repository/provider-endpoints-display-name.test.ts

@@ -0,0 +1,27 @@
+import { describe, expect, test } from "vitest";
+import { deriveDisplayNameFromDomain } from "@/repository/provider-endpoints";
+
+describe("deriveDisplayNameFromDomain", () => {
+  test("uses second-level label before suffix", async () => {
+    expect(await deriveDisplayNameFromDomain("co.yes.vg")).toBe("Yes");
+  });
+
+  test("keeps api prefix handling and capitalization", async () => {
+    expect(await deriveDisplayNameFromDomain("api.openai.com")).toBe("Openai");
+  });
+
+  test("falls back to first label when single part", async () => {
+    expect(await deriveDisplayNameFromDomain("localhost")).toBe("Localhost");
+  });
+
+  test("handles common API prefixes correctly", async () => {
+    expect(await deriveDisplayNameFromDomain("v1.api.anthropic.com")).toBe("Anthropic");
+    expect(await deriveDisplayNameFromDomain("www.example.com")).toBe("Example");
+    expect(await deriveDisplayNameFromDomain("api.anthropic.com")).toBe("Anthropic");
+  });
+
+  test("handles standard domains without prefixes", async () => {
+    expect(await deriveDisplayNameFromDomain("anthropic.com")).toBe("Anthropic");
+    expect(await deriveDisplayNameFromDomain("openai.com")).toBe("Openai");
+  });
+});

+ 156 - 2
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx

@@ -9,9 +9,14 @@ import { createRoot } from "react-dom/client";
 import { NextIntlClientProvider } from "next-intl";
 import { beforeEach, describe, expect, test, vi } from "vitest";
 import { ProviderVendorView } from "@/app/[locale]/settings/providers/_components/provider-vendor-view";
+import type { ProviderDisplay } from "@/types/provider";
 import type { User } from "@/types/user";
 import enMessages from "../../../../messages/en";
 
+vi.mock("next/navigation", () => ({
+  useRouter: () => ({ refresh: vi.fn() }),
+}));
+
 const sonnerMocks = vi.hoisted(() => ({
   toast: {
     success: vi.fn(),
@@ -93,6 +98,61 @@ const ADMIN_USER: User = {
   isEnabled: true,
 };
 
+function makeProviderDisplay(overrides: Partial<ProviderDisplay> = {}): ProviderDisplay {
+  return {
+    id: 1,
+    name: "Provider A",
+    url: "https://api.example.com",
+    maskedKey: "sk-test",
+    isEnabled: true,
+    weight: 1,
+    priority: 1,
+    costMultiplier: 1,
+    groupTag: null,
+    providerType: "claude",
+    providerVendorId: 1,
+    preserveClientIp: false,
+    modelRedirects: null,
+    allowedModels: null,
+    joinClaudePool: true,
+    codexInstructionsStrategy: "auto",
+    mcpPassthroughType: "none",
+    mcpPassthroughUrl: null,
+    limit5hUsd: null,
+    limitDailyUsd: null,
+    dailyResetMode: "fixed",
+    dailyResetTime: "00:00",
+    limitWeeklyUsd: null,
+    limitMonthlyUsd: null,
+    limitTotalUsd: null,
+    limitConcurrentSessions: 1,
+    maxRetryAttempts: null,
+    circuitBreakerFailureThreshold: 1,
+    circuitBreakerOpenDuration: 60,
+    circuitBreakerHalfOpenSuccessThreshold: 1,
+    proxyUrl: null,
+    proxyFallbackToDirect: false,
+    firstByteTimeoutStreamingMs: 0,
+    streamingIdleTimeoutMs: 0,
+    requestTimeoutNonStreamingMs: 0,
+    websiteUrl: null,
+    faviconUrl: null,
+    cacheTtlPreference: null,
+    context1mPreference: null,
+    codexReasoningEffortPreference: null,
+    codexReasoningSummaryPreference: null,
+    codexTextVerbosityPreference: null,
+    codexParallelToolCallsPreference: null,
+    tpm: null,
+    rpm: null,
+    rpd: null,
+    cc: null,
+    createdAt: "2026-01-01",
+    updatedAt: "2026-01-01",
+    ...overrides,
+  };
+}
+
 function loadMessages() {
   return {
     common: enMessages.common,
@@ -163,7 +223,7 @@ describe("ProviderVendorView: VendorTypeCircuitControl 仅在熔断时展示关
 
     const { unmount } = renderWithProviders(
       <ProviderVendorView
-        providers={[]}
+        providers={[makeProviderDisplay()]}
         currentUser={ADMIN_USER}
         enableMultiProviderTypes={true}
         healthStatus={{}}
@@ -202,7 +262,7 @@ describe("ProviderVendorView: VendorTypeCircuitControl 仅在熔断时展示关
 
     const { unmount } = renderWithProviders(
       <ProviderVendorView
-        providers={[]}
+        providers={[makeProviderDisplay()]}
         currentUser={ADMIN_USER}
         enableMultiProviderTypes={true}
         healthStatus={{}}
@@ -226,3 +286,97 @@ describe("ProviderVendorView: VendorTypeCircuitControl 仅在熔断时展示关
     unmount();
   });
 });
+
+describe("ProviderVendorView vendor list", () => {
+  beforeEach(() => {
+    queryClient = new QueryClient({
+      defaultOptions: {
+        queries: { retry: false },
+        mutations: { retry: false },
+      },
+    });
+    vi.clearAllMocks();
+    document.body.innerHTML = "";
+  });
+
+  test("vendors with zero providers are hidden", async () => {
+    providerEndpointsActionMocks.getProviderVendors.mockResolvedValueOnce([
+      {
+        id: 1,
+        displayName: "Vendor A",
+        websiteDomain: "vendor.example",
+        websiteUrl: "https://vendor.example",
+        faviconUrl: null,
+        createdAt: "2026-01-01",
+        updatedAt: "2026-01-01",
+      },
+    ]);
+
+    const { unmount } = renderWithProviders(
+      <ProviderVendorView
+        providers={[]}
+        currentUser={ADMIN_USER}
+        enableMultiProviderTypes={true}
+        healthStatus={{}}
+        statistics={{}}
+        statisticsLoading={false}
+        currencyCode="USD"
+      />
+    );
+
+    await flushTicks(6);
+
+    expect(document.body.textContent || "").not.toContain("Vendor A");
+
+    unmount();
+  });
+});
+
+describe("ProviderVendorView endpoints table", () => {
+  beforeEach(() => {
+    queryClient = new QueryClient({
+      defaultOptions: {
+        queries: { retry: false },
+        mutations: { retry: false },
+      },
+    });
+    vi.clearAllMocks();
+    document.body.innerHTML = "";
+  });
+
+  test("renders endpoints and toggles enabled status", async () => {
+    const provider = makeProviderDisplay();
+    const { unmount } = renderWithProviders(
+      <ProviderVendorView
+        providers={[provider]}
+        currentUser={ADMIN_USER}
+        enableMultiProviderTypes={true}
+        healthStatus={{}}
+        statistics={{}}
+        statisticsLoading={false}
+        currencyCode="USD"
+      />
+    );
+
+    await flushTicks(6);
+
+    expect(document.body.textContent || "").toContain("https://api.example.com/v1");
+
+    const endpointRow = Array.from(document.querySelectorAll("tr")).find((row) =>
+      row.textContent?.includes("https://api.example.com/v1")
+    );
+    expect(endpointRow).toBeDefined();
+
+    const switchEl = endpointRow?.querySelector<HTMLElement>("[data-slot='switch']");
+    expect(switchEl).not.toBeNull();
+    switchEl?.click();
+
+    await flushTicks(2);
+
+    expect(providerEndpointsActionMocks.editProviderEndpoint).toHaveBeenCalledWith(
+      expect.objectContaining({ endpointId: 1, isEnabled: false })
+    );
+
+    unmount();
+  });
+});