perf(panel): kanban virtualization + row memoization + scorecards batch endpoint (#594)

* perf(panel): kanban virtualization + row memoization + scorecards batch endpoint

The audit's remaining phases: the kanban card lists render through
@tanstack/react-virtual windows (columns are the dnd drop targets, cards
only drag — no sortable conflict) with memoized columns/cards and a
stabilized handleAction; the task table's desktop row and mobile card are
extracted and memoized (the table itself already client-paginates to 100).
Backend: the per-member scorecard N+1 (~20 requests x 3 queries per poll)
collapses into GET /dashboard/metrics/members backed by
get_all_member_scorecards with grouped rollup/overlay SQL shared with the
single-agent path.

* test(metrics): shared-DB-safe scorecard tests — unique seeds, delta assertions

The two new batch-scorecard tests assumed a private DB: fixed ceo/system
slugs collided with other tests' seeds (ix_agents_slug) and a global
exactly-one CEO lookup + exact roster count broke in the one-process
suite. Unique slugs, subset/disjoint assertions, dead count constant
dropped.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-19 19:42:24 +02:00
committed by GitHub
co-authored by Renn F
parent db04674342
commit bab53e31ca
14 changed files with 1009 additions and 442 deletions
@@ -14,7 +14,7 @@ const { mockOrg, mockCeo, mockMember, mockAgents } = vi.hoisted(() => ({
vi.mock("@/hooks/use-observability", () => ({
useOrgScorecard: mockOrg,
useCeoScorecard: mockCeo,
useMemberScorecard: mockMember,
useAllMemberScorecards: mockMember,
}));
vi.mock("@/hooks/use-agents", () => ({
@@ -96,18 +96,21 @@ describe("ScorecardsTabContent", () => {
isLoading: false,
});
mockMember.mockReturnValue({
data: member({
id: "dev1",
name: "be-dev-1",
tasks_completed: 7,
first_pass_yield: 0.8,
active_runtime_hours: 4.2,
qa_pass_rate: 0.9,
escalations: 1,
blocked_others: 2,
utilization: 0.6,
}),
data: [
member({
id: "dev1",
name: "be-dev-1",
tasks_completed: 7,
first_pass_yield: 0.8,
active_runtime_hours: 4.2,
qa_pass_rate: 0.9,
escalations: 1,
blocked_others: 2,
utilization: 0.6,
}),
],
isLoading: false,
isError: false,
});
mockAgents.mockReturnValue({
data: [
@@ -164,8 +167,10 @@ describe("ScorecardsTabContent", () => {
expect(
screen.getByText(/failed to load organization metrics/i),
).toBeInTheDocument();
// The member row shows a failed marker rather than a perpetual skeleton
// (exact lowercase text, distinct from the org card's message).
expect(screen.getByText("failed to load")).toBeInTheDocument();
// The batched member-scorecard fetch surfaces one table-level banner
// rather than a per-row error (or an endless per-row skeleton).
expect(
screen.getByText(/failed to load member scorecards/i),
).toBeInTheDocument();
});
});
+27 -17
View File
@@ -1,5 +1,6 @@
"use client";
import { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
@@ -13,12 +14,12 @@ import {
TableRow,
} from "@/components/ui/table";
import {
useAllMemberScorecards,
useCeoScorecard,
useMemberScorecard,
useOrgScorecard,
} from "@/hooks/use-observability";
import { useAgents } from "@/hooks/use-agents";
import { AgentRole, type Agent } from "@/types";
import { AgentRole, type Agent, type MemberScorecard } from "@/types";
function pctOrNa(rate: number | null): string {
return rate === null ? "n/a" : (rate * 100).toFixed(0) + "%";
@@ -32,20 +33,18 @@ function hoursOrDash(seconds: number): string {
return (seconds / 3600).toFixed(1) + "h";
}
/** One member's row — each row self-fetches its rollup scorecard. */
function MemberRow({ agent }: { agent: Agent }) {
const { data, isLoading, isError } = useMemberScorecard(agent.id);
if (isError) {
return (
<TableRow>
<TableCell>{agent.name || agent.slug}</TableCell>
<TableCell colSpan={8} className="text-muted-foreground text-xs">
failed to load
</TableCell>
</TableRow>
);
}
if (isLoading || !data) {
/** One member's row. Data comes from the batched useAllMemberScorecards
* fetch (one request for the whole table) rather than each row self-fetching
* — ~20 agents used to mean ~20 parallel `/metrics/member/{id}` requests
* (each 3 DB queries) on every table poll. */
function MemberRow({
agent,
data,
}: {
agent: Agent;
data: MemberScorecard | undefined;
}) {
if (!data) {
return (
<TableRow>
<TableCell>{agent.name || agent.slug}</TableCell>
@@ -172,6 +171,12 @@ export function ScorecardsTabContent() {
const members = (agents ?? []).filter(
(a) => a.role !== AgentRole.CEO && a.role !== AgentRole.SYSTEM,
);
const { data: scorecards, isError: scorecardsError } =
useAllMemberScorecards();
const scorecardById = useMemo(
() => new Map((scorecards ?? []).map((s) => [s.id, s])),
[scorecards],
);
return (
<div className="space-y-6">
<Card>
@@ -197,6 +202,11 @@ export function ScorecardsTabContent() {
<CardTitle>Members</CardTitle>
</CardHeader>
<CardContent>
{scorecardsError && (
<p className="text-muted-foreground mb-2 text-sm">
Failed to load member scorecards.
</p>
)}
<Table>
<TableHeader>
<TableRow>
@@ -245,7 +255,7 @@ export function ScorecardsTabContent() {
</TableHeader>
<TableBody>
{members.map((a) => (
<MemberRow key={a.id} agent={a} />
<MemberRow key={a.id} agent={a} data={scorecardById.get(a.id)} />
))}
</TableBody>
</Table>