Skip to content

Commit 1b8f48b

Browse files
gcmsgclaude
andcommitted
feat: enrich console agent detail page + fix dark mode chart visibility
Add identity/trust, skills, categories, reviews sections to the provider agent detail page. Fix reputation chart line being nearly invisible in dark mode by adjusting --chart-1 color and adding gradient area fill. Backend: expose public_key, skills, categories, verified, reputation_score, review_summary in provider agent detail API. Add IsAgentVerified to reputation engine and GetCategoriesByAgent to review service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ab5ac5d commit 1b8f48b

File tree

12 files changed

+250
-26
lines changed

12 files changed

+250
-26
lines changed

internal/reputation/engine.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,8 @@ func (e *Engine) SetVerified(ctx context.Context, agentID string) error {
175175
func (e *Engine) UnsetVerified(ctx context.Context, agentID string) error {
176176
return e.store.UnsetAgentVerified(ctx, agentID)
177177
}
178+
179+
// IsVerified returns whether the agent is verified and, if so, when.
180+
func (e *Engine) IsVerified(ctx context.Context, agentID string) (bool, *time.Time, error) {
181+
return e.store.IsAgentVerified(ctx, agentID)
182+
}

internal/reputation/postgres.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ func (s *PostgresStore) UnsetAgentVerified(ctx context.Context, agentID string)
160160
return err
161161
}
162162

163+
// IsAgentVerified returns whether the agent is verified and, if so, when.
164+
func (s *PostgresStore) IsAgentVerified(ctx context.Context, agentID string) (bool, *time.Time, error) {
165+
var verified bool
166+
var verifiedAt *time.Time
167+
err := s.db.QueryRowContext(ctx,
168+
`SELECT COALESCE(verified, FALSE), verified_at FROM agents WHERE id = $1`, agentID,
169+
).Scan(&verified, &verifiedAt)
170+
if err != nil {
171+
return false, nil, err
172+
}
173+
return verified, verifiedAt, nil
174+
}
175+
163176
// ListStaleOnlineAgents returns IDs of agents whose status is online but
164177
// whose last heartbeat is older than the given timeout.
165178
func (s *PostgresStore) ListStaleOnlineAgents(ctx context.Context, timeout time.Duration) ([]string, error) {

internal/reputation/sqlite.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,24 @@ func (s *SQLiteStore) UnsetAgentVerified(ctx context.Context, agentID string) er
174174
return err
175175
}
176176

177+
// IsAgentVerified returns whether the agent is verified and, if so, when.
178+
func (s *SQLiteStore) IsAgentVerified(ctx context.Context, agentID string) (bool, *time.Time, error) {
179+
var verified bool
180+
var verifiedAt *string
181+
err := s.db.QueryRowContext(ctx,
182+
`SELECT COALESCE(verified, 0), verified_at FROM agents WHERE id = ?`, agentID,
183+
).Scan(&verified, &verifiedAt)
184+
if err != nil {
185+
return false, nil, err
186+
}
187+
if verifiedAt != nil {
188+
if t, err := time.Parse(time.RFC3339, *verifiedAt); err == nil {
189+
return verified, &t, nil
190+
}
191+
}
192+
return verified, nil, nil
193+
}
194+
177195
// ListStaleOnlineAgents returns IDs of agents whose status is online but
178196
// whose last heartbeat is older than the given timeout.
179197
func (s *SQLiteStore) ListStaleOnlineAgents(ctx context.Context, timeout time.Duration) ([]string, error) {

internal/reputation/store.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type Store interface {
5353
// UnsetAgentVerified removes the verified status from an agent.
5454
UnsetAgentVerified(ctx context.Context, agentID string) error
5555

56+
// IsAgentVerified returns whether the agent is verified and, if so, when.
57+
IsAgentVerified(ctx context.Context, agentID string) (bool, *time.Time, error)
58+
5659
// ListStaleOnlineAgents returns IDs of agents whose status is online but
5760
// whose last heartbeat is older than the given timeout.
5861
ListStaleOnlineAgents(ctx context.Context, timeout time.Duration) ([]string, error)

internal/review/service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ func (s *Service) ListCategories(ctx context.Context) ([]Category, error) {
114114
return s.store.ListCategories(ctx)
115115
}
116116

117+
// GetCategoriesByAgent returns the categories associated with an agent.
118+
func (s *Service) GetCategoriesByAgent(ctx context.Context, agentID string) ([]Category, error) {
119+
return s.store.GetCategoriesByAgent(ctx, agentID)
120+
}
121+
117122
// ListReports returns abuse reports with optional status filter.
118123
func (s *Service) ListReports(ctx context.Context, status string, limit, offset int) ([]AbuseReport, int, error) {
119124
return s.store.ListReports(ctx, status, limit, offset)

internal/server/provider_handler.go

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,23 +165,61 @@ func (s *HTTPServer) handleProviderGetAgent(w http.ResponseWriter, r *http.Reque
165165
// Enrich response with access flags.
166166
flags, _ := s.registry.GetAccessFlags(r.Context(), id)
167167
resp := map[string]any{
168-
"id": card.ID,
169-
"name": card.Name,
170-
"description": card.Description,
171-
"version": card.Version,
172-
"capabilities": card.Capabilities,
173-
"protocols": card.Protocols,
174-
"status": card.Status,
175-
"endpoint_url": card.Endpoint.URL,
176-
"auth_type": card.Auth.Type,
177-
"tags": card.PeerClaw.Tags,
178-
"created_at": card.RegisteredAt,
179-
"updated_at": card.LastHeartbeat,
168+
"id": card.ID,
169+
"name": card.Name,
170+
"description": card.Description,
171+
"version": card.Version,
172+
"capabilities": card.Capabilities,
173+
"protocols": card.Protocols,
174+
"status": card.Status,
175+
"endpoint_url": card.Endpoint.URL,
176+
"auth_type": card.Auth.Type,
177+
"tags": card.PeerClaw.Tags,
178+
"created_at": card.RegisteredAt,
179+
"updated_at": card.LastHeartbeat,
180+
"public_key": card.PublicKey,
181+
"skills": card.Skills,
182+
"registered_at": card.RegisteredAt,
183+
"last_heartbeat": card.LastHeartbeat,
180184
}
181185
if flags != nil {
182186
resp["playground_enabled"] = flags.PlaygroundEnabled
183187
resp["visibility"] = flags.Visibility
184188
}
189+
190+
// Enrich with live reputation score and verified status.
191+
if s.reputation != nil {
192+
score, _ := s.reputation.GetScore(r.Context(), card.ID)
193+
resp["reputation_score"] = score
194+
verified, verifiedAt, err := s.reputation.IsVerified(r.Context(), card.ID)
195+
if err == nil {
196+
resp["verified"] = verified
197+
if verifiedAt != nil {
198+
resp["verified_at"] = verifiedAt
199+
}
200+
}
201+
}
202+
203+
// Enrich with review summary and categories.
204+
if s.reviewService != nil {
205+
summary, err := s.reviewService.GetSummary(r.Context(), card.ID)
206+
if err == nil && summary != nil {
207+
resp["review_summary"] = map[string]any{
208+
"average_rating": summary.AverageRating,
209+
"total_reviews": summary.TotalReviews,
210+
"distribution": summary.Distribution,
211+
}
212+
}
213+
categories, err := s.reviewService.GetCategoriesByAgent(r.Context(), card.ID)
214+
if err == nil && len(categories) > 0 {
215+
slugs := make([]string, len(categories))
216+
for i, c := range categories {
217+
slugs[i] = c.Slug
218+
}
219+
resp["categories"] = slugs
220+
}
221+
}
222+
185223
s.jsonResponse(w, http.StatusOK, resp)
186224
}
187225

web/app/src/components/public/ReputationChart.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTranslation } from "react-i18next"
22
import {
3-
LineChart,
4-
Line,
3+
AreaChart,
4+
Area,
55
XAxis,
66
YAxis,
77
CartesianGrid,
@@ -35,7 +35,13 @@ export function ReputationChart({ events }: { events: ReputationEvent[] }) {
3535

3636
return (
3737
<ResponsiveContainer width="100%" height={240}>
38-
<LineChart data={data}>
38+
<AreaChart data={data}>
39+
<defs>
40+
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
41+
<stop offset="0%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
42+
<stop offset="100%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
43+
</linearGradient>
44+
</defs>
3945
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
4046
<XAxis
4147
dataKey="time"
@@ -57,16 +63,17 @@ export function ReputationChart({ events }: { events: ReputationEvent[] }) {
5763
}}
5864
labelStyle={{ color: "hsl(var(--foreground))" }}
5965
/>
60-
<Line
66+
<Area
6167
type="monotone"
6268
dataKey="score"
6369
stroke="hsl(var(--chart-1))"
64-
strokeWidth={2}
70+
strokeWidth={2.5}
71+
fill="url(#chartGradient)"
6572
dot={{ r: 4, fill: "hsl(var(--chart-1))", strokeWidth: 0 }}
6673
activeDot={{ r: 6, fill: "hsl(var(--chart-1))", strokeWidth: 2, stroke: "hsl(var(--card))" }}
6774
connectNulls
6875
/>
69-
</LineChart>
76+
</AreaChart>
7077
</ResponsiveContainer>
7178
)
7279
}

web/app/src/hooks/use-provider.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export interface ProviderAgent {
3030
updated_at: string
3131
playground_enabled?: boolean
3232
visibility?: string
33+
public_key?: string
34+
skills?: Array<{ name: string; description?: string }>
35+
categories?: string[]
36+
verified?: boolean
37+
verified_at?: string
38+
registered_at?: string
39+
last_heartbeat?: string
40+
reputation_score?: number
41+
review_summary?: {
42+
average_rating: number
43+
total_reviews: number
44+
distribution: number[]
45+
}
3346
}
3447

3548
export interface ProviderDashboardData {

web/app/src/i18n/locales/en.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,14 @@
211211
"deleting": "Deleting...",
212212
"deleteConfirm": "Are you sure you want to delete this agent? This action cannot be undone.",
213213
"name": "Name",
214-
"calls": "Calls"
214+
"calls": "Calls",
215+
"identityTrust": "Identity & Trust",
216+
"publicKey": "Public Key",
217+
"agentId": "Agent ID",
218+
"reputationScore": "Reputation Score",
219+
"skills": "Skills",
220+
"categories": "Categories",
221+
"copied": "Copied"
215222
},
216223
"claim": {
217224
"title": "Claim Tokens",

web/app/src/i18n/locales/zh.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,14 @@
211211
"deleting": "删除中...",
212212
"deleteConfirm": "确定要删除此 Agent 吗?此操作不可撤销。",
213213
"name": "名称",
214-
"calls": "调用"
214+
"calls": "调用",
215+
"identityTrust": "身份与信任",
216+
"publicKey": "公钥",
217+
"agentId": "Agent ID",
218+
"reputationScore": "信誉评分",
219+
"skills": "技能",
220+
"categories": "分类",
221+
"copied": "已复制"
215222
},
216223
"claim": {
217224
"title": "声明令牌",

0 commit comments

Comments
 (0)