Skip to content

Commit 4e12c5d

Browse files
github-actions[bot]carhartlewisclaude
authored
chore(people): move heavy data fetches off the shared server page
* perf(people): move heavy data fetches off the shared server page The People page server component was re-running every expensive query (Fleet API, S3 signed URL, device-agent + Fleet + org-chart queries) on every tab click because `router.replace()` triggers a full re-render. Strip page.tsx to just auth + the member query needed to decide whether the Tasks tab shows, and push tab-specific data into client components that fetch via SWR hooks. Also: - Add Next.js API routes for agent devices and fleet hosts (`/api/people/agent-devices`, `/api/people/fleet-hosts`) so client hooks can call them. - `DevicesTabContent`, `OrgChartTabContent`, and `TeamMembersClient` now use `useAgentDevices` / `useFleetHosts` / `useOrgChart`. SWR's shared cache means devices are fetched once across People + Devices tabs. - Consolidate the four per-scope `PeopleFindings` sections into a single `Findings` tab with a unified list. `CreateFindingSheet` accepts `scopeOptions` so the picker lives inside the form. - `FindingsOverview` now links people-scoped findings to `?tab=findings` since they no longer live on the per-area tabs. Also align the auditor view with the rest of the app: - Drop the redundant `(overview)/layout.tsx` wrapper (PageLayout already handles the container). - Use `PageLayout header={<PageHeader>}` pattern from `/frameworks`. - Extract `ExportEvidenceButton` and fix the nested `<button>` hydration error by styling PopoverTrigger directly (same pattern as `TasksPageClient`). - Replace the custom `<Section>` with `@trycompai/design-system` `Section` + `Stack`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(people): address review feedback for SWR scoping, perf, and data correctness Scope SWR keys by orgId to prevent stale cross-org data, pre-index fleet policy results for O(1) lookups, batch Fleet API calls to limit concurrency, validate both x/y in org chart positions, and filter deactivated members from agent device queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Lewis Carhart <lewis@trycomp.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 28daac5 commit 4e12c5d

File tree

25 files changed

+964
-1000
lines changed

25 files changed

+964
-1000
lines changed

apps/app/src/app/(app)/[orgId]/auditor/(overview)/components/AuditorView.tsx

Lines changed: 33 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,6 @@
1-
'use client';
2-
3-
import { downloadAllEvidenceZip } from '@/lib/evidence-download';
4-
import {
5-
Button,
6-
Popover,
7-
PopoverContent,
8-
PopoverDescription,
9-
PopoverHeader,
10-
PopoverTitle,
11-
PopoverTrigger,
12-
Switch,
13-
} from '@trycompai/design-system';
14-
import { ArrowDown } from '@trycompai/design-system/icons';
1+
import { Section, Stack } from '@trycompai/design-system';
152
import { Download } from 'lucide-react';
163
import Image from 'next/image';
17-
import { useParams } from 'next/navigation';
18-
import { useState } from 'react';
19-
import { toast } from 'sonner';
204

215
interface AuditorViewProps {
226
initialContent: Record<string, string>;
@@ -35,84 +19,34 @@ export function AuditorView({
3519
cSuite,
3620
reportSignatory,
3721
}: AuditorViewProps) {
38-
const params = useParams();
39-
const orgId = params.orgId as string;
40-
const [isDownloading, setIsDownloading] = useState(false);
41-
const [includeJson, setIncludeJson] = useState(false);
42-
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
43-
44-
const handleDownloadAllEvidence = async () => {
45-
setIsDownloading(true);
46-
try {
47-
await downloadAllEvidenceZip({
48-
organizationName,
49-
includeJson,
50-
});
51-
toast.success('Evidence package downloaded successfully');
52-
setIsPopoverOpen(false);
53-
} catch (err) {
54-
toast.error('Failed to download evidence. Please try again.');
55-
console.error('Evidence download error:', err);
56-
} finally {
57-
setIsDownloading(false);
58-
}
59-
};
60-
6122
return (
62-
<div className="flex flex-col gap-10">
63-
{/* Header */}
64-
<div className="flex items-center justify-between">
65-
<div className="flex items-center gap-4">
23+
<Stack gap="xl">
24+
<Section title="Company Information">
25+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
6626
{logoUrl && (
67-
<a
68-
href={logoUrl}
69-
download={`${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_logo`}
70-
className="group relative h-14 w-14 shrink-0 overflow-hidden rounded-lg border bg-background transition-all hover:shadow-md"
71-
title="Download logo"
72-
>
73-
<Image src={logoUrl} alt={`${organizationName} logo`} fill className="object-contain" />
74-
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
75-
<Download className="h-4 w-4 text-white" />
76-
</div>
77-
</a>
27+
<InfoCell
28+
label="Logo"
29+
className="lg:border-r lg:border-border lg:pr-6"
30+
value={
31+
<a
32+
href={logoUrl}
33+
download={`${organizationName.replace(/[^a-zA-Z0-9]/g, '_')}_logo`}
34+
className="group relative block h-14 w-14 overflow-hidden rounded-lg border bg-background transition-all hover:shadow-md"
35+
title="Download logo"
36+
>
37+
<Image
38+
src={logoUrl}
39+
alt={`${organizationName} logo`}
40+
fill
41+
className="object-contain"
42+
/>
43+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
44+
<Download className="h-4 w-4 text-white" />
45+
</div>
46+
</a>
47+
}
48+
/>
7849
)}
79-
<div>
80-
<h1 className="text-foreground text-xl font-semibold tracking-tight">
81-
{organizationName}
82-
</h1>
83-
<p className="text-muted-foreground text-sm">Company Overview</p>
84-
</div>
85-
</div>
86-
87-
{/* Download All Evidence Button */}
88-
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
89-
<PopoverTrigger style={{ cursor: 'pointer' }}>
90-
<Button variant="outline">Export All Evidence</Button>
91-
</PopoverTrigger>
92-
<PopoverContent align="end" side="bottom" sideOffset={8}>
93-
<PopoverHeader>
94-
<PopoverTitle>Export Options</PopoverTitle>
95-
<PopoverDescription>Download all task evidence as ZIP</PopoverDescription>
96-
</PopoverHeader>
97-
<div className="flex items-center justify-between gap-3 py-1">
98-
<span className="text-sm">Include raw JSON files</span>
99-
<Switch checked={includeJson} onCheckedChange={(checked) => setIncludeJson(checked)} />
100-
</div>
101-
<Button
102-
iconLeft={<ArrowDown />}
103-
onClick={handleDownloadAllEvidence}
104-
disabled={isDownloading}
105-
width="full"
106-
>
107-
{isDownloading ? 'Preparing…' : 'Export'}
108-
</Button>
109-
</PopoverContent>
110-
</Popover>
111-
</div>
112-
113-
{/* Company Information */}
114-
<Section title="Company Information">
115-
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
11650
<InfoCell
11751
label="Employees"
11852
value={employeeCount || '—'}
@@ -126,13 +60,9 @@ export function AuditorView({
12660
<div>
12761
<div className="flex items-baseline gap-2">
12862
<span className="font-medium">{reportSignatory.fullName}</span>
129-
<span className="text-muted-foreground text-xs">
130-
{reportSignatory.jobTitle}
131-
</span>
132-
</div>
133-
<div className="text-muted-foreground text-xs mt-0.5">
134-
{reportSignatory.email}
63+
<span className="text-muted-foreground text-xs">{reportSignatory.jobTitle}</span>
13564
</div>
65+
<div className="text-muted-foreground text-xs mt-0.5">{reportSignatory.email}</div>
13666
</div>
13767
) : (
13868
'—'
@@ -141,7 +71,6 @@ export function AuditorView({
14171
/>
14272
<InfoCell
14373
label="Executive Team"
144-
className="sm:col-span-2 lg:col-span-1"
14574
value={
14675
cSuite.length > 0 ? (
14776
<div className="space-y-1">
@@ -160,9 +89,8 @@ export function AuditorView({
16089
</div>
16190
</Section>
16291

163-
{/* Business Overview */}
16492
<Section title="Business Overview">
165-
<div className="space-y-6">
93+
<Stack gap="lg">
16694
<ContentRow
16795
title="Company Background & Overview of Operations"
16896
content={initialContent['Company Background & Overview of Operations']}
@@ -172,15 +100,13 @@ export function AuditorView({
172100
content={initialContent['Types of Services Provided']}
173101
/>
174102
<ContentRow title="Mission & Vision" content={initialContent['Mission & Vision']} />
175-
</div>
103+
</Stack>
176104
</Section>
177105

178-
{/* System Architecture */}
179106
<Section title="System Architecture">
180107
<ContentRow title="System Description" content={initialContent['System Description']} />
181108
</Section>
182109

183-
{/* Third Party Dependencies */}
184110
<Section title="Third Party Dependencies">
185111
<div className="grid gap-6 lg:grid-cols-2">
186112
<ContentRow title="Critical Vendors" content={initialContent['Critical Vendors']} />
@@ -190,20 +116,7 @@ export function AuditorView({
190116
/>
191117
</div>
192118
</Section>
193-
</div>
194-
);
195-
}
196-
197-
function Section({ title, children }: { title: string; children: React.ReactNode }) {
198-
return (
199-
<div className="space-y-4">
200-
<div className="flex items-center gap-3 border-b border-border pb-2">
201-
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
202-
{title}
203-
</h2>
204-
</div>
205-
{children}
206-
</div>
119+
</Stack>
207120
);
208121
}
209122

@@ -217,7 +130,7 @@ function InfoCell({
217130
className?: string;
218131
}) {
219132
return (
220-
<div className={className || ''}>
133+
<div className={className}>
221134
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground mb-1.5">
222135
{label}
223136
</div>
@@ -233,9 +146,7 @@ function ContentRow({ title, content }: { title: string; content?: string }) {
233146
<div className="space-y-1.5">
234147
<h3 className="text-sm font-medium text-foreground">{title}</h3>
235148
{hasContent ? (
236-
<p className="text-sm leading-relaxed text-muted-foreground whitespace-pre-wrap">
237-
{content}
238-
</p>
149+
<p className="text-sm leading-relaxed text-muted-foreground whitespace-pre-wrap">{content}</p>
239150
) : (
240151
<p className="text-xs text-muted-foreground/50">Not yet available</p>
241152
)}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client';
2+
3+
import { downloadAllEvidenceZip } from '@/lib/evidence-download';
4+
import {
5+
Button,
6+
Popover,
7+
PopoverContent,
8+
PopoverDescription,
9+
PopoverHeader,
10+
PopoverTitle,
11+
PopoverTrigger,
12+
Switch,
13+
} from '@trycompai/design-system';
14+
import { ArrowDown } from '@trycompai/design-system/icons';
15+
import { useState } from 'react';
16+
import { toast } from 'sonner';
17+
18+
interface ExportEvidenceButtonProps {
19+
organizationName: string;
20+
}
21+
22+
export function ExportEvidenceButton({ organizationName }: ExportEvidenceButtonProps) {
23+
const [isDownloading, setIsDownloading] = useState(false);
24+
const [includeJson, setIncludeJson] = useState(false);
25+
const [isOpen, setIsOpen] = useState(false);
26+
27+
const handleDownload = async () => {
28+
setIsDownloading(true);
29+
try {
30+
await downloadAllEvidenceZip({ organizationName, includeJson });
31+
toast.success('Evidence package downloaded successfully');
32+
setIsOpen(false);
33+
} catch (err) {
34+
toast.error('Failed to download evidence. Please try again.');
35+
console.error('Evidence download error:', err);
36+
} finally {
37+
setIsDownloading(false);
38+
}
39+
};
40+
41+
return (
42+
<Popover open={isOpen} onOpenChange={setIsOpen}>
43+
<PopoverTrigger className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-7 px-2 cursor-pointer">
44+
Export All Evidence
45+
</PopoverTrigger>
46+
<PopoverContent align="end" side="bottom" sideOffset={8}>
47+
<PopoverHeader>
48+
<PopoverTitle>Export Options</PopoverTitle>
49+
<PopoverDescription>Download all task evidence as ZIP</PopoverDescription>
50+
</PopoverHeader>
51+
<div className="flex items-center justify-between gap-3 py-1">
52+
<span className="text-sm">Include raw JSON files</span>
53+
<Switch checked={includeJson} onCheckedChange={setIncludeJson} />
54+
</div>
55+
<Button
56+
iconLeft={<ArrowDown />}
57+
onClick={handleDownload}
58+
disabled={isDownloading}
59+
width="full"
60+
>
61+
{isDownloading ? 'Preparing…' : 'Export'}
62+
</Button>
63+
</PopoverContent>
64+
</Popover>
65+
);
66+
}

apps/app/src/app/(app)/[orgId]/auditor/(overview)/layout.tsx

Lines changed: 0 additions & 4 deletions
This file was deleted.

apps/app/src/app/(app)/[orgId]/auditor/(overview)/page.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import PageWithBreadcrumb from '@/components/pages/PageWithBreadcrumb';
21
import { serverApi } from '@/lib/api-server';
32
import { parseRolesString } from '@/lib/permissions';
3+
import { PageHeader, PageLayout } from '@trycompai/design-system';
44
import { Role } from '@db';
55
import type { Metadata } from 'next';
66
import { notFound, redirect } from 'next/navigation';
77
import { AuditorView } from './components/AuditorView';
8+
import { ExportEvidenceButton } from './components/ExportEvidenceButton';
89

910
interface PeopleMember {
1011
userId: string;
@@ -53,7 +54,7 @@ export default async function AuditorPage({
5354
}: {
5455
params: Promise<{ orgId: string }>;
5556
}) {
56-
const { orgId: organizationId } = await params;
57+
await params;
5758

5859
const [membersRes, orgRes, contextRes] = await Promise.all([
5960
serverApi.get<PeopleApiResponse>('/v1/people'),
@@ -121,25 +122,22 @@ export default async function AuditorPage({
121122
}
122123

123124
return (
124-
<PageWithBreadcrumb
125-
breadcrumbs={[
126-
{
127-
label: 'Auditor View',
128-
href: `/${organizationId}/auditor`,
129-
current: true,
130-
},
131-
]}
125+
<PageLayout
126+
header={
127+
<PageHeader
128+
title={organizationName}
129+
actions={<ExportEvidenceButton organizationName={organizationName} />}
130+
/>
131+
}
132132
>
133133
<AuditorView
134134
initialContent={initialContent}
135-
organizationName={organizationName}
136135
logoUrl={logoUrl}
137-
employeeCount={
138-
initialContent['How many employees do you have?'] || null
139-
}
136+
organizationName={organizationName}
137+
employeeCount={initialContent['How many employees do you have?'] || null}
140138
cSuite={cSuiteData}
141139
reportSignatory={signatoryData}
142140
/>
143-
</PageWithBreadcrumb>
141+
</PageLayout>
144142
);
145143
}

apps/app/src/app/(app)/[orgId]/overview/components/FindingsOverview.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,7 @@ function findingHref(finding: FindingWithTask, organizationId: string): string {
5555
return `/${organizationId}/documents/${finding.evidenceSubmission.formType}?tab=findings`;
5656
}
5757
if (finding.scope) {
58-
const peopleTab =
59-
finding.scope === FindingScope.people
60-
? 'people'
61-
: finding.scope === FindingScope.people_tasks
62-
? 'tasks'
63-
: finding.scope === FindingScope.people_devices
64-
? 'devices'
65-
: finding.scope === FindingScope.people_chart
66-
? 'chart'
67-
: 'people';
68-
return `/${organizationId}/people?tab=${peopleTab}`;
58+
return `/${organizationId}/people?tab=findings`;
6959
}
7060
return `/${organizationId}/overview`;
7161
}

0 commit comments

Comments
 (0)