feat(panel): tooltip sweep — overview, agents, A2A, journals, auditor, metrics

Derivation tips on every key-metric, scorecard, and quality-metrics
figure; the cryptic member-scorecard headers get full decodes; avatar
initials, truncated ids, and toggle buttons gain accessible names;
title-only hints upgrade to the HelpTip idiom throughout.
This commit is contained in:
Renn F
2026-07-15 16:40:13 +02:00
parent c37516f640
commit 3d1c20e95a
33 changed files with 723 additions and 334 deletions
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
const { mockOrg } = vi.hoisted(() => ({ mockOrg: vi.fn() }));
@@ -44,6 +45,13 @@ describe("ScorecardOverviewPanel", () => {
expect(link).toHaveAttribute("href", "/metrics?tab=scorecards");
});
it("explains a metric row's derivation via a hover tooltip", async () => {
const user = userEvent.setup();
render(<ScorecardOverviewPanel />);
await user.hover(screen.getByText("First-pass yield"));
expect(await screen.findByRole("tooltip")).toHaveTextContent(/bounce/i);
});
it("shows a skeleton while loading", () => {
mockOrg.mockReturnValue({ data: undefined, isLoading: true });
const { container } = render(<ScorecardOverviewPanel />);
@@ -2,6 +2,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import {
TrendingUp,
CheckCircle,
@@ -19,6 +20,7 @@ interface MetricItem {
label: string;
icon: React.ReactNode;
format?: (value: number) => string;
tip: string;
}
// Keys must match DashboardService.get_key_metrics() — the shape /dashboard/ceo
@@ -30,24 +32,28 @@ const METRIC_CONFIG: MetricItem[] = [
label: "Velocity (7d)",
icon: <TrendingUp className="h-4 w-4" />,
format: (v) => `${v} tasks`,
tip: "Tasks completed in the last 7 days",
},
{
key: "completion_rate",
label: "Completion Rate",
icon: <CheckCircle className="h-4 w-4" />,
format: (v) => `${Math.round(v * 100)}%`,
tip: "Share of tasks started in the last 7 days that reached completed",
},
{
key: "documentation_coverage",
label: "Documentation Coverage",
icon: <BarChart3 className="h-4 w-4" />,
format: (v) => `${Math.round(v * 100)}%`,
tip: "Share of completed tasks that passed through a documentation step",
},
{
key: "active_blockers",
label: "Active Blockers",
icon: <AlertTriangle className="h-4 w-4" />,
format: (v) => `${v}`,
tip: "Tasks currently in the blocked status right now",
},
];
@@ -70,15 +76,17 @@ export function KeyMetricsPanel({ metrics, isLoading }: KeyMetricsProps) {
const rawValue = metrics?.[m.key];
const value = typeof rawValue === "number" ? rawValue : null;
return (
<div key={m.key} className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{m.icon}
{m.label}
<HelpTip key={m.key} label={m.tip}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{m.icon}
{m.label}
</div>
<span className="font-medium">
{value != null ? (m.format ? m.format(value) : value) : "-"}
</span>
</div>
<span className="font-medium">
{value != null ? (m.format ? m.format(value) : value) : "-"}
</span>
</div>
</HelpTip>
);
})}
</div>
@@ -3,6 +3,7 @@
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useOrgScorecard } from "@/hooks/use-observability";
import {
Trophy,
@@ -25,17 +26,20 @@ interface MetricRowProps {
icon: React.ReactNode;
label: string;
value: string;
tip: string;
}
function MetricRow({ icon, label, value }: MetricRowProps) {
function MetricRow({ icon, label, value, tip }: MetricRowProps) {
return (
<div className="flex items-center justify-between py-1">
<div className="text-muted-foreground flex items-center gap-2 text-sm">
{icon}
{label}
<HelpTip label={tip}>
<div className="flex items-center justify-between py-1">
<div className="text-muted-foreground flex items-center gap-2 text-sm">
{icon}
{label}
</div>
<span className="text-sm font-semibold">{value}</span>
</div>
<span className="text-sm font-semibold">{value}</span>
</div>
</HelpTip>
);
}
@@ -81,26 +85,31 @@ export function ScorecardOverviewPanel() {
icon={<CheckCircle2 className="h-4 w-4" />}
label="Tasks completed (30d)"
value={String(data.tasks_completed)}
tip="Tasks that reached completed org-wide in the last 30 days"
/>
<MetricRow
icon={<Gauge className="h-4 w-4" />}
label="First-pass yield"
value={pctOrNa(data.first_pass_yield)}
tip="Share of completed tasks that shipped without a QA fail, PR-gate fail, PM reject, or CEO reject bounce"
/>
<MetricRow
icon={<Gauge className="h-4 w-4 text-blue-500" />}
label="Throughput / hr"
value={numOrNa(data.effort_throughput_per_hour)}
tip="Tasks completed per hour of active (non-idle) agent runtime"
/>
<MetricRow
icon={<Clock className="h-4 w-4" />}
label="Active effort"
value={data.active_runtime_hours.toFixed(1) + "h"}
tip="Total hours agents spent actively working — idle/waiting time excluded"
/>
<MetricRow
icon={<Coins className="h-4 w-4" />}
label="Cost"
value={"$" + data.cost_usd.toFixed(2)}
tip="Total provider-priced token cost across all sessions in the period"
/>
</div>
)}
@@ -3,6 +3,7 @@
import { TeamHealth } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { HealthIndicator } from "./health-indicator";
import { Users, AlertTriangle, TrendingUp } from "lucide-react";
@@ -10,6 +11,12 @@ interface TeamHealthCardProps {
health: TeamHealth;
}
const HEALTH_STATUS_TIP: Record<TeamHealth["status"], string> = {
ok: "Healthy — blocked ratio is low and work is flowing",
slow: "Slow — a meaningful share of this team's tasks are blocked",
critical: "Critical — most of this team's tasks are blocked; needs attention",
};
export function TeamHealthCard({ health }: TeamHealthCardProps) {
const teamName = health.team.replace(/_/g, " ");
@@ -18,7 +25,11 @@ export function TeamHealthCard({ health }: TeamHealthCardProps) {
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-lg capitalize">{teamName}</CardTitle>
<HealthIndicator status={health.status} size="sm" />
<HelpTip label={HEALTH_STATUS_TIP[health.status]}>
<span>
<HealthIndicator status={health.status} size="sm" />
</span>
</HelpTip>
</div>
</CardHeader>
<CardContent className="space-y-3">
@@ -56,17 +67,19 @@ export function TeamHealthCard({ health }: TeamHealthCardProps) {
{/* Blocked Ratio */}
{health.blocked_ratio > 0 && (
<div className="pt-2 border-t">
<Badge
variant={
health.blocked_ratio > 0.3
? "destructive"
: health.blocked_ratio > 0.1
? "secondary"
: "outline"
}
>
{Math.round(health.blocked_ratio * 100)}% blocked
</Badge>
<HelpTip label="Share of this team's active tasks currently blocked">
<Badge
variant={
health.blocked_ratio > 0.3
? "destructive"
: health.blocked_ratio > 0.1
? "secondary"
: "outline"
}
>
{Math.round(health.blocked_ratio * 100)}% blocked
</Badge>
</HelpTip>
</div>
)}
</CardContent>
@@ -3,6 +3,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useUsageSummary } from "@/hooks/use-usage";
import { useUsageStore } from "@/store/usage-store";
import type { ConnectionState } from "@/lib/websocket/connection";
@@ -32,22 +33,25 @@ interface MetricRowProps {
label: string;
value: string;
sub?: React.ReactNode;
tip?: string;
}
function MetricRow({ icon, label, value, sub }: MetricRowProps) {
function MetricRow({ icon, label, value, sub, tip }: MetricRowProps) {
return (
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{icon}
{label}
<HelpTip label={tip}>
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{icon}
{label}
</div>
<div className="flex items-center gap-1">
<span className="font-semibold text-sm transition-all duration-300 ease-in-out">
{value}
</span>
{sub}
</div>
</div>
<div className="flex items-center gap-1">
<span className="font-semibold text-sm transition-all duration-300 ease-in-out">
{value}
</span>
{sub}
</div>
</div>
</HelpTip>
);
}
@@ -113,10 +117,12 @@ export function UsageOverviewPanel() {
<Coins className="h-5 w-5" />
Token Usage &amp; Cost
</CardTitle>
<Badge className={badge.className}>
{badge.icon}
{badge.label}
</Badge>
<HelpTip label="Connection to the live /ws/system stream — Polling means it's down and figures refresh via periodic HTTP fetch instead">
<Badge className={badge.className}>
{badge.icon}
{badge.label}
</Badge>
</HelpTip>
</div>
</CardHeader>
<CardContent>
@@ -132,16 +138,19 @@ export function UsageOverviewPanel() {
icon={<Zap className="h-4 w-4" />}
label="Tokens (input)"
value={tokensInput != null ? fmt(tokensInput) : "—"}
tip="Prompt/context tokens sent to the model across all agent sessions in this period"
/>
<MetricRow
icon={<Zap className="h-4 w-4 text-muted-foreground" />}
label="Tokens (output)"
value={tokensOutput != null ? fmt(tokensOutput) : "—"}
tip="Tokens generated by the model in response, across all agent sessions in this period"
/>
<MetricRow
icon={<Coins className="h-4 w-4" />}
label="Total cost"
value={totalCost != null ? fmtCost(totalCost) : "—"}
tip="Provider-priced cost for input + output tokens this period (local/Ollama sessions cost $0)"
/>
<MetricRow
icon={
@@ -166,11 +175,13 @@ export function UsageOverviewPanel() {
</span>
) : undefined
}
tip="Change in total cost compared to the immediately preceding period of the same length"
/>
<MetricRow
icon={<Activity className="h-4 w-4 text-blue-500" />}
label="Period"
value={periodLabel ?? "—"}
tip="The rolling time window these figures are aggregated over"
/>
</div>
)}