Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
327ee31
fix(frontend): ask only for metrics the installation's catalog offers
Aug 6, 2026
c3530eb
fix(frontend): the headline row shows what a person is measured on
Aug 6, 2026
e007918
fix(frontend): attention surfaces only what the section cards do not
Aug 6, 2026
392dcc1
fix(frontend): one finding paints one mark on a KPI tile
Aug 6, 2026
58cd020
fix(frontend): make the person page readable — space, affordances, dates
Aug 6, 2026
b809261
fix(frontend): one mark per row on a section card
Aug 6, 2026
631e128
fix(frontend): finish the deduplication, align the numbers, drop a st…
Aug 6, 2026
2e6963e
fix(frontend): keep the attention value next to the name it measures
Aug 6, 2026
b3c8eed
fix(frontend): a section card is one list, led by what matters
Aug 6, 2026
37740a9
fix(frontend): a section card lists rows that have a value
Aug 6, 2026
649ef5b
feat(frontend): the nav says which section to open; the page stops re…
Aug 6, 2026
b72a467
fix(frontend): never offer to open a person above the viewer
Aug 6, 2026
6910e11
feat(frontend): attention reports changes, and the page names its cohort
Aug 6, 2026
f3d2854
docs(frontend): describe the conditions, not an environment
Aug 6, 2026
548978e
fix(frontend): answer the review on the person page
Aug 6, 2026
0f35bfa
fix(frontend): stop vite re-optimizing deps mid test run
Aug 7, 2026
b71c785
feat(frontend): every metric explains itself on hover
Aug 7, 2026
e739db9
fix(frontend): the attention block names a standing, not only a change
Aug 7, 2026
8c1d23f
fix(frontend): the value states a quantity, the line states the verdict
Aug 7, 2026
f144bda
fix(frontend): a row of tiles fills the row it is in
Aug 7, 2026
56e83cd
fix(frontend): a comparison that failed is an error, not silence
Aug 7, 2026
681d519
Merge branch 'main' into fix/metric-keys-not-in-catalog
dzarlax Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/frontend/src/components/portal/ai-cost-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ vi.mock("@/queries/team-view", () => ({
vi.mock("@/queries/member-grid", () => ({ useMemberGridData: () => mocks.grid }));
vi.mock("@/queries/metric-results", () => ({ useMetricCollection: () => mocks.tools }));
vi.mock("@/hooks/use-portal-period", () => ({
usePortalPeriod: () => ({ period: "week", dateRange: { start: "2026-07-20", end: "2026-07-26" } }),
usePortalPeriod: () => ({ period: "week", dateRange: { from: "2026-07-20", to: "2026-07-26" } }),
}));


Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/components/portal/attention-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Link } from "@tanstack/react-router";
import { AlertTriangle, ArrowDownRight, Sparkles, TrendingDown } from "lucide-react";
import { AlertTriangle, ArrowDownRight, ChevronRight, Sparkles, TrendingDown } from "lucide-react";

import { Card, CardContent } from "@/components/ui/card";
import type { AttentionFlag, FlagKind } from "@/lib/insight/attention-flags";
Expand Down Expand Up @@ -83,6 +83,12 @@ export function AttentionList({
</span>
<span className="shrink-0 font-medium tabular-nums">{f.valueText}</span>
<span className="truncate text-xs text-muted-foreground">{f.reason}</span>
{/* Standing affordance — the row opens that person's page, and
a border alone does not say so. */}
<ChevronRight
className="ml-auto size-3.5 shrink-0 text-muted-foreground/50"
aria-hidden
/>
</Link>
);
})}
Expand Down
71 changes: 71 additions & 0 deletions src/frontend/src/components/portal/context-pane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
zone: { activeZone: "overview", activePerson: "boss@x" },
standings: [] as Array<{
id: string;
title: string;
status: string;
phrase: string;
hasData: boolean;
isPending: boolean;
}>,
}));

vi.mock("@/lib/portal/use-active-zone", () => ({ useActiveZone: () => mocks.zone }));
Expand All @@ -31,6 +39,13 @@ import {
usePortalLens,
} from "@/lib/portal/portal-nav";
import { SidebarProvider } from "@/components/ui/sidebar";
// The sections nav asks where the person stands so it can mark each section;
// the standings come from the section screens' own queries, which this test
// has no reason to run.
vi.mock("@/lib/portal/use-person-sections", () => ({
usePersonSectionStandings: () => mocks.standings,
}));

import { ContextPane } from "./context-pane";

const pane = () => render(<SidebarProvider><ContextPane /></SidebarProvider>);
Expand All @@ -47,6 +62,7 @@ beforeEach(() => {
dispatchEvent: () => false,
})) as unknown as typeof window.matchMedia;
mocks.zone = { activeZone: "overview", activePerson: "boss@x" };
mocks.standings = [];
act(() => {
portalRouter.set({ zone: undefined });
portalRouter.set({ item: undefined });
Expand Down Expand Up @@ -108,4 +124,59 @@ describe("ContextPane", () => {
expect(screen.getByText("Pick a person")).toBeInTheDocument();
expect(screen.getByText("At a glance")).toBeInTheDocument();
});

it("marks a section with where the person stands in it", () => {
// The mark is the whole point of this nav: it says which section is worth
// opening. Its colour and its reason have to reach the reader.
mocks.zone = { activeZone: "person", activePerson: "boss@x" };
mocks.standings = [
{
id: "git_output",
title: "Git output",
status: "bad",
phrase: "4 of 6 behind peers",
hasData: true,
isPending: false,
},
];
pane();
const button = screen.getByTitle("4 of 6 behind peers");
expect(button.querySelector(".bg-destructive")).not.toBeNull();
});

it("says a section has nothing rather than colouring it", () => {
mocks.zone = { activeZone: "person", activePerson: "boss@x" };
mocks.standings = [
{
id: "git_output",
title: "Git output",
status: "neutral",
phrase: "no peer data",
hasData: false,
isPending: false,
},
];
pane();
const button = screen.getByTitle("No data this period");
expect(button.querySelector(".bg-muted-foreground\\/30")).not.toBeNull();
});

it("draws no mark while the standings are still loading", () => {
// A pending section drawn grey would read as "nothing here" — an answer
// the hook has not given yet.
mocks.zone = { activeZone: "person", activePerson: "boss@x" };
mocks.standings = [
{
id: "git_output",
title: "Git output",
status: "neutral",
phrase: "",
hasData: false,
isPending: true,
},
];
pane();
const button = screen.getByText("Git output").closest("button")!;
expect(button.querySelector("span[aria-hidden]")).toBeNull();
});
});
57 changes: 43 additions & 14 deletions src/frontend/src/components/portal/context-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { GROUPS } from "@/lib/insight/groups";
import { usePersonSectionStandings } from "@/lib/portal/use-person-sections";
import { STATUS_BG_CLASS } from "@/lib/status";
import { lensEntry } from "@/lib/portal/lens-configs";
import { useShellLayout } from "@/lib/portal/use-shell-layout";
import { useZoneNav } from "@/lib/portal/use-zone-nav";
Expand Down Expand Up @@ -485,6 +487,11 @@ function PersonSectionsNav() {
const { setItem } = usePortalNavActions();
const dismiss = useDismissDrawer();
const active = usePortalItem();
const { activePerson } = useActiveZone();
// Costs no request: these are the section screens' own queries, so
// react-query serves them from cache.
const standings = usePersonSectionStandings(activePerson);
const standingById = new Map(standings.map((st) => [st.id as string, st]));
const groups = GROUPS;
const groupIds = groups.map((g) => g.id) as string[];
const glance = active == null || !groupIds.includes(active);
Expand All @@ -505,20 +512,42 @@ function PersonSectionsNav() {
<span>At a glance</span>
</SidebarMenuButton>
</SidebarMenuItem>
{groups.map((g) => (
<SidebarMenuItem key={g.id}>
<SidebarMenuButton
isActive={active === g.id}
onClick={() => {
setItem(g.id);
dismiss();
}}
>
<Layers />
<span>{g.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
{groups.map((g) => {
const standing = standingById.get(g.id as string);
return (
<SidebarMenuItem key={g.id}>
<SidebarMenuButton
isActive={active === g.id}
onClick={() => {
setItem(g.id);
dismiss();
}}
title={
standing?.hasData === false
? "No data this period"
: standing?.phrase
}
>
<Layers />
<span className="min-w-0 flex-1 truncate">{g.title}</span>
{/* The mark that answers "which section is worth opening",
beside the thing you click. Grey means the section has
nothing this period — worth knowing before you open it. */}
{standing && !standing.isPending ? (
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
standing.hasData
? STATUS_BG_CLASS[standing.status]
: "bg-muted-foreground/30",
)}
aria-hidden
/>
) : null}
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ vi.mock("@/queries/metric-results", () => ({
vi.mock("@/hooks/use-portal-period", () => ({
usePortalPeriod: () => ({
period: "week",
dateRange: { start: "2026-07-20", end: "2026-07-26" },
dateRange: { from: "2026-07-20", to: "2026-07-26" },
}),
}));
// Charts are exercised by the browser/storybook project; here they'd render
Expand Down
Loading
Loading