mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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 & 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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user