feat(providers): add provider-level Codex pool activity monitor (#539)

Add provider-scoped runtime monitor for Codex pool owners:

- New `GET /v1/providers/{id}/codex-pool-activity` endpoint aggregates pool health across all agents
- New Pool Activity section on provider detail page (pool owners only)
- Shows aggregate member health, recent requests, top agents with drill-down links
- 7-day time window on provider-scoped span query for performance
- Reuses `buildCodexPoolActivity()` — zero duplicated aggregation logic
- i18n complete (en/vi/zh), accessible markup

Closes #499
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-03-29 12:46:23 +07:00
1 parent 789c9613be
commit a524b457ba
13 files changed
+596 -3

No files matched your search

+6
View File
@@ -62,6 +62,12 @@ func wireHTTP(stores *store.Stores, defaultWorkspace, dataDir, bundledSkillsDir
if stores.MCP != nil {
providersH.SetMCPServerLookup(buildMCPServerLookup(stores.MCP))
}
if stores.Tracing != nil {
providersH.SetTracingStore(stores.Tracing)
}
if stores.Agents != nil {
providersH.SetAgentStore(stores.Agents)
}
}
var teamEventsH *httpapi.TeamEventsHandler
+15
View File
@@ -29,6 +29,8 @@ type ProvidersHandler struct {
cliMu sync.Mutex // serializes Claude CLI provider create to prevent duplicates
msgBus *bus.MessageBus
sysConfigStore store.SystemConfigStore
tracingStore store.TracingStore // optional: for provider-scoped pool activity
agents store.AgentCRUDStore // optional: for provider pool activity agent lookup
}
// NewProvidersHandler creates a handler for provider management endpoints.
@@ -59,6 +61,16 @@ func (h *ProvidersHandler) SetAPIBaseFallback(fn func(providerType string) strin
h.apiBaseFallback = fn
}
// SetTracingStore sets the tracing store for provider-scoped pool activity.
func (h *ProvidersHandler) SetTracingStore(ts store.TracingStore) {
h.tracingStore = ts
}
// SetAgentStore sets the agent store for provider pool activity agent lookup.
func (h *ProvidersHandler) SetAgentStore(as store.AgentCRUDStore) {
h.agents = as
}
// resolveAPIBase returns the provider's api_base, falling back to config/env if empty.
func (h *ProvidersHandler) resolveAPIBase(p *store.LLMProviderData) string {
if p.APIBase != "" {
@@ -98,6 +110,9 @@ func (h *ProvidersHandler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /v1/providers/{id}/verify", h.auth(h.handleVerifyProvider))
mux.HandleFunc("POST /v1/providers/{id}/verify-embedding", h.auth(h.handleVerifyEmbedding))
// Provider-scoped Codex pool activity monitor
mux.HandleFunc("GET /v1/providers/{id}/codex-pool-activity", h.auth(h.handleProviderCodexPoolActivity))
// Embedding system status
mux.HandleFunc("GET /v1/embedding/status", h.auth(h.handleEmbeddingStatus))
@@ -0,0 +1,177 @@
package http
import (
"context"
"log/slog"
"net/http"
"slices"
"strconv"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/i18n"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// providerCodexPoolAgentCount tracks per-agent request counts for provider-scoped activity.
type providerCodexPoolAgentCount struct {
AgentID uuid.UUID `json:"agent_id"`
AgentKey string `json:"agent_key,omitempty"`
RequestCount int `json:"request_count"`
}
// handleProviderCodexPoolActivity returns pool activity aggregated across all agents
// that use this provider's Codex pool. Only valid for chatgpt_oauth pool owners.
func (h *ProvidersHandler) handleProviderCodexPoolActivity(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
if h.tracingStore == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
"error": i18n.T(locale, i18n.MsgInvalidRequest, "tracing store unavailable"),
})
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "provider")})
return
}
provider, err := h.store.GetProvider(r.Context(), id)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "provider", id.String())})
return
}
if provider.ProviderType != store.ProviderChatGPTOAuth {
writeJSON(w, http.StatusOK, emptyProviderPoolActivityResponse())
return
}
// Resolve pool providers from provider settings (capped at 20 to bound query size)
const maxPoolCandidates = 20
settings := store.ParseChatGPTOAuthProviderSettings(provider.Settings)
poolCandidates := []string{provider.Name}
strategy := store.ChatGPTOAuthStrategyPrimaryFirst
if settings != nil && settings.CodexPool != nil {
if settings.CodexPool.Strategy != "" {
strategy = settings.CodexPool.Strategy
}
for _, name := range settings.CodexPool.ExtraProviderNames {
if name != "" && !slices.Contains(poolCandidates, name) {
poolCandidates = append(poolCandidates, name)
if len(poolCandidates) >= maxPoolCandidates {
break
}
}
}
}
// Filter to registered Codex providers
poolProviders := registeredCodexPoolProviders(h.providerReg, provider.TenantID, poolCandidates)
if len(poolProviders) == 0 {
writeJSON(w, http.StatusOK, emptyProviderPoolActivityResponse())
return
}
limit := 18
if raw := r.URL.Query().Get("limit"); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 50 {
limit = parsed
}
}
statsLimit := maxInt(limit, codexPoolRuntimeHealthSampleSize)
rawSpans, err := h.tracingStore.ListCodexPoolSpansByProviders(r.Context(), provider.TenantID, poolProviders, statsLimit)
if err != nil {
slog.Error("providers.codex_pool_activity", "provider", provider.Name, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgFailedToList, "pool activity")})
return
}
// Filter spans to those with routing evidence matching pool providers
spans := make([]store.CodexPoolSpan, 0, len(rawSpans))
agentRequestCounts := make(map[uuid.UUID]int)
for _, item := range rawSpans {
if evidence := providers.ExtractChatGPTOAuthRoutingEvidence(item.Metadata); evidence.HasData() {
if !providerInPool(poolProviders, evidence.SelectedProvider) && !providerInPool(poolProviders, evidence.ServingProvider) {
continue
}
} else if !providerInPool(poolProviders, item.Provider) {
continue
}
spans = append(spans, item.CodexPoolSpan)
agentRequestCounts[item.AgentID]++
}
providerCounts, recent := buildCodexPoolActivity(poolProviders, spans)
if len(recent) > limit {
recent = recent[:limit]
}
// Build top agents list sorted by request count
topAgents := buildTopAgents(r.Context(), agentRequestCounts, h.agents)
writeJSON(w, http.StatusOK, map[string]any{
"strategy": strategy,
"pool_providers": poolProviders,
"stats_sample_size": len(spans),
"provider_counts": providerCounts,
"recent_requests": recent,
"top_agents": topAgents,
})
}
func emptyProviderPoolActivityResponse() map[string]any {
return map[string]any{
"strategy": store.ChatGPTOAuthStrategyPrimaryFirst,
"pool_providers": []string{},
"stats_sample_size": 0,
"provider_counts": []codexPoolProviderCount{},
"recent_requests": []codexPoolRecentRequest{},
"top_agents": []providerCodexPoolAgentCount{},
}
}
// buildTopAgents converts agent request counts into a sorted slice, resolving agent keys.
func buildTopAgents(ctx context.Context, counts map[uuid.UUID]int, agentStore store.AgentCRUDStore) []providerCodexPoolAgentCount {
if len(counts) == 0 {
return []providerCodexPoolAgentCount{}
}
agentIDs := make([]uuid.UUID, 0, len(counts))
for id := range counts {
agentIDs = append(agentIDs, id)
}
keyByID := make(map[uuid.UUID]string, len(agentIDs))
if agentStore != nil {
if agents, err := agentStore.GetByIDs(ctx, agentIDs); err != nil {
slog.Warn("providers.codex_pool_activity.resolve_agents", "error", err, "count", len(agentIDs))
} else {
for _, ag := range agents {
keyByID[ag.ID] = ag.AgentKey
}
}
}
result := make([]providerCodexPoolAgentCount, 0, len(counts))
for id, count := range counts {
result = append(result, providerCodexPoolAgentCount{
AgentID: id,
AgentKey: keyByID[id],
RequestCount: count,
})
}
slices.SortFunc(result, func(a, b providerCodexPoolAgentCount) int {
return b.RequestCount - a.RequestCount
})
if len(result) > 10 {
result = result[:10]
}
return result
}
+60
View File
@@ -67,3 +67,63 @@ func (s *PGTracingStore) ListCodexPoolSpans(ctx context.Context, agentID, tenant
}
return spans, nil
}
const listCodexPoolSpansByProvidersQuery = `
SELECT
sp.id,
sp.trace_id,
sp.start_time,
COALESCE(sp.duration_ms, 0),
sp.status,
COALESCE(sp.provider, ''),
COALESCE(sp.model, ''),
COALESCE(sp.metadata, '{}'::jsonb),
t.agent_id
FROM spans sp
JOIN traces t ON t.id = sp.trace_id
WHERE t.tenant_id = $1
AND t.parent_trace_id IS NULL
AND sp.tenant_id = $1
AND sp.span_type = 'llm_call'
AND sp.start_time > NOW() - INTERVAL '7 days'
AND (
sp.provider = ANY($2)
OR COALESCE(sp.metadata->'chatgpt_oauth_routing'->>'selected_provider', '') = ANY($2)
OR COALESCE(sp.metadata->'chatgpt_oauth_routing'->>'serving_provider', '') = ANY($2)
)
ORDER BY sp.start_time DESC
LIMIT $3`
// ListCodexPoolSpansByProviders returns recent LLM call spans across all agents for the given pool providers.
func (s *PGTracingStore) ListCodexPoolSpansByProviders(ctx context.Context, tenantID uuid.UUID, poolProviders []string, limit int) ([]store.CodexPoolProviderSpan, error) {
rows, err := s.db.QueryContext(ctx, listCodexPoolSpansByProvidersQuery, tenantID, pq.Array(poolProviders), limit)
if err != nil {
return nil, err
}
defer rows.Close()
spans := make([]store.CodexPoolProviderSpan, 0, limit)
for rows.Next() {
var item store.CodexPoolProviderSpan
var metadata json.RawMessage
if err := rows.Scan(
&item.SpanID,
&item.TraceID,
&item.StartedAt,
&item.DurationMS,
&item.Status,
&item.Provider,
&item.Model,
&metadata,
&item.AgentID,
); err != nil {
return nil, err
}
item.Metadata = metadata
spans = append(spans, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return spans, nil
}
+5
View File
@@ -255,3 +255,8 @@ func (s *SQLiteTracingStore) DeleteTracesOlderThan(ctx context.Context, cutoff t
func (s *SQLiteTracingStore) ListCodexPoolSpans(_ context.Context, _, _ uuid.UUID, _ []string, _ int) ([]store.CodexPoolSpan, error) {
return nil, nil
}
// ListCodexPoolSpansByProviders is not supported in SQLite (Codex pool is a standard-edition feature).
func (s *SQLiteTracingStore) ListCodexPoolSpansByProviders(_ context.Context, _ uuid.UUID, _ []string, _ int) ([]store.CodexPoolProviderSpan, error) {
return nil, nil
}
+10
View File
@@ -162,4 +162,14 @@ type TracingStore interface {
// ListCodexPoolSpans returns recent LLM call spans for agents using Codex OAuth pool providers.
ListCodexPoolSpans(ctx context.Context, agentID, tenantID uuid.UUID, poolProviders []string, limit int) ([]CodexPoolSpan, error)
// ListCodexPoolSpansByProviders returns recent LLM call spans across all agents
// that used any of the given pool providers. Used for provider-scoped activity monitoring.
ListCodexPoolSpansByProviders(ctx context.Context, tenantID uuid.UUID, poolProviders []string, limit int) ([]CodexPoolProviderSpan, error)
}
// CodexPoolProviderSpan extends CodexPoolSpan with the agent ID for provider-scoped aggregation.
type CodexPoolProviderSpan struct {
CodexPoolSpan
AgentID uuid.UUID
}
+9 -1
View File
@@ -222,7 +222,15 @@
"oauthManagedByHint": "This alias is managed by provider {{provider}}. Edit pool membership there instead of on this account.",
"codexPoolDefaultsTitle": "Pool Defaults",
"codexPoolDefaultsDescription": "Define the reusable OpenAI Codex pool owned by this alias for agents that use it as their primary provider.",
"oauthDisplayNameRecommendation": "Add a display name if you want clearer labels when multiple OpenAI Codex OAuth accounts appear in the pool."
"oauthDisplayNameRecommendation": "Add a display name if you want clearer labels when multiple OpenAI Codex OAuth accounts appear in the pool.",
"poolActivityTitle": "Pool Activity",
"poolActivityDescription": "Aggregate runtime activity across all agents using this provider's Codex pool.",
"poolActivityRefresh": "Refresh",
"poolActivityTopAgents": "Agents",
"poolActivityTopAgentsTitle": "Top agents using this pool",
"poolActivityRequests": "req",
"poolActivityEmpty": "No activity yet",
"poolActivityEmptyDesc": "Pool activity will appear here once agents start routing requests through this pool."
},
"embedding": {
"enable": "Enable Embedding",
+9 -1
View File
@@ -226,7 +226,15 @@
"oauthManagedByHint": "Bí danh này đang được quản lý bởi provider {{provider}}. Hãy chỉnh membership ở provider đó thay vì tại tài khoản này.",
"codexPoolDefaultsTitle": "Thiết lập pool mặc định",
"codexPoolDefaultsDescription": "Định nghĩa pool OpenAI Codex dùng lại do chính bí danh này sở hữu cho các agent dùng nó làm provider chính.",
"oauthDisplayNameRecommendation": "Thêm tên hiển thị nếu bạn muốn nhãn rõ ràng hơn khi nhiều tài khoản OpenAI Codex OAuth xuất hiện trong pool."
"oauthDisplayNameRecommendation": "Thêm tên hiển thị nếu bạn muốn nhãn rõ ràng hơn khi nhiều tài khoản OpenAI Codex OAuth xuất hiện trong pool.",
"poolActivityTitle": "Hoạt động pool",
"poolActivityDescription": "Hoạt động runtime tổng hợp trên tất cả agent sử dụng pool Codex của provider này.",
"poolActivityRefresh": "Làm mới",
"poolActivityTopAgents": "Agent",
"poolActivityTopAgentsTitle": "Agent sử dụng pool nhiều nhất",
"poolActivityRequests": "req",
"poolActivityEmpty": "Chưa có hoạt động",
"poolActivityEmptyDesc": "Hoạt động pool sẽ xuất hiện ở đây khi agent bắt đầu gửi request qua pool này."
},
"list": {
"poolOwner": "Chủ pool",
+9 -1
View File
@@ -244,7 +244,15 @@
"oauthManagedByHint": "这个别名由 provider {{provider}} 管理。请到那个 provider 上编辑池成员,而不是在这个账户上修改。",
"codexPoolDefaultsTitle": "默认账户池",
"codexPoolDefaultsDescription": "为把这个别名作为主 provider 的 agent 定义由该别名拥有的可复用 OpenAI Codex 账户池。",
"oauthDisplayNameRecommendation": "如果您希望多个 OpenAI Codex OAuth 账户在池里显示得更清楚,可以补充显示名称。"
"oauthDisplayNameRecommendation": "如果您希望多个 OpenAI Codex OAuth 账户在池里显示得更清楚,可以补充显示名称。",
"poolActivityTitle": "池活动",
"poolActivityDescription": "汇总所有使用该 provider Codex 池的 agent 的运行时活动。",
"poolActivityRefresh": "刷新",
"poolActivityTopAgents": "Agent",
"poolActivityTopAgentsTitle": "使用该池最多的 agent",
"poolActivityRequests": "请求",
"poolActivityEmpty": "暂无活动",
"poolActivityEmptyDesc": "当 agent 开始通过此池路由请求时,池活动将在此显示。"
},
"list": {
"poolOwner": "池拥有者",
+1
View File
@@ -7,6 +7,7 @@ export const queryKeys = {
models: (providerId: string) => ["providers", providerId, "models"] as const,
chatgptOAuthStatuses: (providerKeys: string[]) => ["providers", "chatgpt-oauth-statuses", ...providerKeys] as const,
chatgptOAuthQuotas: (providerNames: string[]) => ["providers", "chatgpt-oauth-quotas", ...providerNames] as const,
codexPoolActivity: (providerId: string, limit: number) => ["providers", providerId, "codex-pool-activity", limit] as const,
},
agents: {
all: ["agents"] as const,
@@ -0,0 +1,56 @@
import { useQuery } from "@tanstack/react-query";
import { useHttp } from "@/hooks/use-ws";
import { queryKeys } from "@/lib/query-keys";
import type {
CodexPoolProviderCount,
CodexPoolRecentRequest,
} from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity";
import type { EffectiveChatGPTOAuthRoutingStrategy } from "@/types/agent";
export interface ProviderCodexPoolAgentCount {
agent_id: string;
agent_key?: string;
request_count: number;
}
interface ProviderCodexPoolActivityResponse {
strategy: EffectiveChatGPTOAuthRoutingStrategy;
pool_providers: string[];
stats_sample_size: number;
provider_counts: CodexPoolProviderCount[];
recent_requests: CodexPoolRecentRequest[];
top_agents: ProviderCodexPoolAgentCount[];
}
const EMPTY: ProviderCodexPoolActivityResponse = {
strategy: "primary_first",
pool_providers: [],
stats_sample_size: 0,
provider_counts: [],
recent_requests: [],
top_agents: [],
};
export function useProviderCodexPoolActivity(
providerId: string,
limit = 18,
enabled = true,
) {
const http = useHttp();
const query = useQuery({
queryKey: queryKeys.providers.codexPoolActivity(providerId, limit),
enabled: enabled && Boolean(providerId),
staleTime: 5_000,
queryFn: () =>
http.get<ProviderCodexPoolActivityResponse>(
`/v1/providers/${providerId}/codex-pool-activity`,
{ limit: String(limit) },
),
});
return {
...query,
data: query.data ?? EMPTY,
};
}
@@ -34,6 +34,8 @@ import {
ChatGPTOAuthRoutingSection,
} from "@/pages/agents/agent-detail/config-sections";
import type { CodexPoolEntry } from "@/pages/agents/agent-detail/codex-pool-activity-panel";
import { useProviderCodexPoolActivity } from "../hooks/use-provider-codex-pool-activity";
import { ProviderPoolActivitySection } from "./provider-pool-activity-section";
interface ProviderOverviewProps {
provider: ProviderData;
@@ -311,6 +313,14 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
});
}, [canEditPoolRouting, provider.name, providerByName, quotaByName, quotaProviderNames, statusByName]);
// Provider-scoped pool activity (only for pool owners)
const isPoolOwner = canEditPoolRouting && poolEntries.length > 0;
const {
data: poolActivity,
isFetching: poolActivityFetching,
refetch: refreshPoolActivity,
} = useProviderCodexPoolActivity(provider.id, 8, isPoolOwner);
const { verifyEmbedding, embVerifying, embResult, resetEmb } = useProviderVerify();
useEffect(() => { resetEmb(); }, [embModel, embDimensions, resetEmb]);
@@ -464,6 +474,21 @@ export function ProviderOverview({ provider, onUpdate }: ProviderOverviewProps)
/>
) : null}
{isPoolOwner ? (
<ProviderPoolActivitySection
provider={provider}
providerCounts={poolActivity.provider_counts}
recentRequests={poolActivity.recent_requests}
topAgents={poolActivity.top_agents}
statsSampleSize={poolActivity.stats_sample_size}
fetching={poolActivityFetching}
onRefresh={() => void refreshPoolActivity()}
providerByName={providerByName}
statusByName={statusByName}
quotaByName={quotaByName}
/>
) : null}
{showApiKey ? (
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
<h3 className="text-sm font-medium">{t("detail.apiKeySection")}</h3>
@@ -0,0 +1,214 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Activity, ExternalLink, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/shared/empty-state";
import { cn } from "@/lib/utils";
import type { CodexPoolProviderCount } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity";
import { CodexPoolMemberCard } from "@/pages/agents/agent-detail/codex-pool-member-card";
import { CodexPoolRecentRequestsList } from "@/pages/agents/agent-detail/codex-pool-recent-requests-list";
import type { CodexPoolEntry } from "@/pages/agents/agent-detail/codex-pool-entry-types";
import type { ChatGPTOAuthAvailability } from "../hooks/use-chatgpt-oauth-provider-statuses";
import type { ChatGPTOAuthProviderQuota } from "../hooks/use-chatgpt-oauth-provider-quotas";
import type { ProviderData } from "../hooks/use-providers";
import type { ProviderCodexPoolAgentCount } from "../hooks/use-provider-codex-pool-activity";
import type { CodexPoolRecentRequest } from "@/pages/agents/agent-detail/hooks/use-codex-pool-activity";
function MonitorStat({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-md border bg-background/70 px-2 py-1">
<p className="text-[9px] font-medium uppercase tracking-wide text-muted-foreground xl:text-[10px]">
{label}
</p>
<p className="mt-0.5 text-[13px] font-semibold leading-tight tabular-nums xl:text-sm">
{value}
</p>
</div>
);
}
interface ProviderPoolActivitySectionProps {
provider: ProviderData;
providerCounts: CodexPoolProviderCount[];
recentRequests: CodexPoolRecentRequest[];
topAgents: ProviderCodexPoolAgentCount[];
statsSampleSize: number;
fetching: boolean;
onRefresh: () => void;
providerByName: Map<string, ProviderData>;
statusByName: Map<string, { availability: ChatGPTOAuthAvailability }>;
quotaByName: Map<string, ChatGPTOAuthProviderQuota | null>;
}
export function ProviderPoolActivitySection({
provider,
providerCounts,
recentRequests,
topAgents,
statsSampleSize,
fetching,
onRefresh,
providerByName,
statusByName,
quotaByName,
}: ProviderPoolActivitySectionProps) {
const { t } = useTranslation("providers");
const { t: ta } = useTranslation("agents");
const entries = useMemo<CodexPoolEntry[]>(() => {
return providerCounts.map((c) => {
const p = providerByName.get(c.provider_name);
const availability: ChatGPTOAuthAvailability =
statusByName.get(c.provider_name)?.availability ??
(p?.enabled === false ? "disabled" : "needs_sign_in");
return {
name: c.provider_name,
label: p?.display_name || c.provider_name,
availability,
role: c.provider_name === provider.name ? "preferred" as const : "extra" as const,
requestCount: c.request_count,
directSelectionCount: c.direct_selection_count,
failoverServeCount: c.failover_serve_count,
successCount: c.success_count,
failureCount: c.failure_count,
consecutiveFailures: c.consecutive_failures,
successRate: c.success_rate,
healthScore: c.health_score,
healthState: c.health_state ?? "idle",
lastSelectedAt: c.last_selected_at,
lastFailoverAt: c.last_failover_at,
lastUsedAt: c.last_used_at,
lastSuccessAt: c.last_success_at,
lastFailureAt: c.last_failure_at,
providerHref: p?.id ? `/providers/${p.id}` : undefined,
quota: quotaByName.get(c.provider_name),
};
});
}, [provider.name, providerByName, providerCounts, quotaByName, statusByName]);
const failoverCount = recentRequests.filter((r) => r.used_failover).length;
return (
<section className="space-y-3 rounded-lg border p-3 sm:p-4 overflow-hidden">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium">{t("detail.poolActivityTitle")}</h3>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 gap-1.5 px-2"
onClick={onRefresh}
disabled={fetching}
aria-busy={fetching}
>
<RefreshCw className={cn("h-3.5 w-3.5", fetching && "animate-spin")} />
{t("detail.poolActivityRefresh")}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{t("detail.poolActivityDescription")}
</p>
{/* Summary stats */}
<div className="grid gap-1.5 grid-cols-2 sm:grid-cols-4">
<MonitorStat
label={ta("chatgptOAuthRouting.metrics.poolSize")}
value={String(entries.length)}
/>
<MonitorStat
label={ta("chatgptOAuthRouting.metrics.observedSample")}
value={String(statsSampleSize)}
/>
<MonitorStat
label={t("detail.poolActivityTopAgents")}
value={String(topAgents.length)}
/>
<MonitorStat
label={ta("chatgptOAuthRouting.metrics.failovers")}
value={String(failoverCount)}
/>
</div>
{/* Pool member cards */}
{entries.length > 0 ? (
<div className="grid auto-rows-min gap-2 [grid-template-columns:repeat(auto-fit,minmax(min(100%,12.25rem),1fr))]">
{entries.map((entry) => (
<CodexPoolMemberCard key={entry.name} entry={entry} showProviderLinks />
))}
</div>
) : (
<EmptyState
icon={Activity}
title={t("detail.poolActivityEmpty")}
description={t("detail.poolActivityEmptyDesc")}
className="py-6"
/>
)}
{/* Recent requests */}
{recentRequests.length > 0 ? (
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-2">
<h4 className="text-xs font-medium text-muted-foreground">
{ta("chatgptOAuthRouting.sequenceTitle")}
</h4>
<Badge variant="outline" className="text-[10px]">
{ta("chatgptOAuthRouting.recentRequestsCount", {
count: recentRequests.length,
})}
</Badge>
</div>
<CodexPoolRecentRequestsList
recentRequests={recentRequests}
loading={fetching && recentRequests.length === 0}
compact
/>
</div>
) : null}
{/* Top agents */}
{topAgents.length > 0 ? (
<div className="space-y-1.5">
<h4 className="text-xs font-medium text-muted-foreground">
{t("detail.poolActivityTopAgentsTitle")}
</h4>
<ul className="space-y-1 list-none p-0 m-0" role="list">
{topAgents.map((agent) => (
<li
key={agent.agent_id}
className="flex items-center justify-between rounded-md border bg-muted/30 px-2.5 py-1.5 text-xs"
>
<div className="flex items-center gap-1.5">
{agent.agent_key ? (
<Link
to={`/agents/${agent.agent_id}/codex-pool`}
className="font-medium text-foreground hover:underline"
>
{agent.agent_key}
</Link>
) : (
<span className="font-mono text-muted-foreground">
{agent.agent_id.slice(0, 8)}
</span>
)}
<Link
to={`/agents/${agent.agent_id}/codex-pool`}
className="text-muted-foreground hover:text-foreground"
aria-label={`View pool details for ${agent.agent_key || agent.agent_id}`}
>
<ExternalLink className="h-3 w-3" />
</Link>
</div>
<span className="tabular-nums text-muted-foreground">
{agent.request_count} {t("detail.poolActivityRequests")}
</span>
</li>
))}
</ul>
</div>
) : null}
</section>
);
}