diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.test.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.test.tsx index 7edce89d8..40a72f0fa 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.test.tsx @@ -156,33 +156,43 @@ describe('EmployeeTasks device compliance badge', () => { expect(screen.getByText('Stale')).toBeInTheDocument(); }); - it('sets stale badge title tooltip based on daysSinceLastCheckIn', () => { - const { rerender } = renderWithDevice( - makeDevice({ complianceStatus: 'stale', daysSinceLastCheckIn: 9 }), - ); - expect(screen.getByText('Stale (9d)').closest('[title]')?.getAttribute('title')).toBe( - 'No check-in in 9 days', - ); + it('renders a stale-explainer tooltip trigger for a stale device', () => { + renderWithDevice(makeDevice({ complianceStatus: 'stale', daysSinceLastCheckIn: 9 })); + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); - rerender( - , + it('renders the stale-explainer tooltip trigger when daysSinceLastCheckIn is null (never reported)', () => { + renderWithDevice( + makeDevice({ + complianceStatus: 'stale', + daysSinceLastCheckIn: null, + lastCheckIn: null, + }), ); - expect(screen.getByText('Stale').closest('[title]')?.getAttribute('title')).toBe( - 'No check-ins recorded', + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a compliant device', () => { + renderWithDevice(makeDevice({ complianceStatus: 'compliant' })); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a non-compliant device', () => { + renderWithDevice( + makeDevice({ + complianceStatus: 'non_compliant', + isCompliant: false, + diskEncryptionEnabled: false, + }), ); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); }); }); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx index be68e5506..5b6529cea 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx @@ -4,6 +4,7 @@ import type { TrainingVideo } from '@/lib/data/training-videos'; import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db'; import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip'; import { Badge, Section, @@ -14,6 +15,7 @@ import { TabsTrigger, Text, } from '@trycompai/design-system'; +import { Information } from '@trycompai/design-system/icons'; import { AlertCircle, Award, CheckCircle2, Download, Info } from 'lucide-react'; import type { FleetPolicy, Host } from '../../devices/types'; import type { DeviceWithChecks } from '../../devices/types'; @@ -56,18 +58,35 @@ function staleLabel(daysSinceLastCheckIn: number | null): string { return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`; } -function staleTitle(daysSinceLastCheckIn: number | null): string { +function staleTooltipCopy(daysSinceLastCheckIn: number | null): string { return daysSinceLastCheckIn === null - ? 'No check-ins recorded' - : `No check-in in ${daysSinceLastCheckIn} days`; + ? "This device's CompAI agent hasn't reported any check-ins, so we can't verify its current compliance. Ask the employee to install or activate the agent." + : "This device's CompAI agent hasn't reported in over 7 days, so we can't verify its current compliance. Ask the employee to update or reinstall the agent."; } function DeviceComplianceBadge({ device }: { device: DeviceWithChecks }) { if (device.complianceStatus === 'stale') { return ( - - {staleLabel(device.daysSinceLastCheckIn)} - +
+ {staleLabel(device.daysSinceLastCheckIn)} + + + + + + + {staleTooltipCopy(device.daysSinceLastCheckIn)} + + + +
); } if (device.complianceStatus === 'compliant') { diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx index b0a6c75f0..9e7fff392 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx @@ -60,7 +60,9 @@ const baseMember = { const noop = vi.fn(); -function renderMemberRow(deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed') { +function renderMemberRow( + deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed', +) { return render( @@ -109,6 +111,41 @@ describe('MemberRow device status', () => { expect(screen.getByText('Non-Compliant').className).toContain('text-foreground'); }); + it('shows "Stale" with gray dot and muted label when deviceStatus is stale', () => { + const { container } = renderMemberRow('stale'); + expect(screen.getByText('Stale')).toBeInTheDocument(); + expect(screen.getByText('Stale').className).toContain('text-muted-foreground'); + expect(container.querySelector('.bg-gray-400')).toBeInTheDocument(); + }); + + it('renders an info tooltip trigger next to the Stale label', () => { + renderMemberRow('stale'); + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); + + it('does not render the Stale info tooltip for compliant devices', () => { + renderMemberRow('compliant'); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + + it('does not render the Stale info tooltip for non-compliant devices', () => { + renderMemberRow('non-compliant'); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + + it('does not render the Stale info tooltip for not-installed devices', () => { + renderMemberRow('not-installed'); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + it('does not show device status for platform admin', () => { const adminMember = { ...baseMember, @@ -175,8 +212,13 @@ describe('MemberRow device status', () => { expect(yellowDot).toBeInTheDocument(); u2(); - const { container: c3 } = renderMemberRow('not-installed'); - const redDot = c3.querySelector('.bg-red-400'); + const { container: c3, unmount: u3 } = renderMemberRow('stale'); + const grayDot = c3.querySelector('.bg-gray-400'); + expect(grayDot).toBeInTheDocument(); + u3(); + + const { container: c4 } = renderMemberRow('not-installed'); + const redDot = c4.querySelector('.bg-red-400'); expect(redDot).toBeInTheDocument(); }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 1fb8d5af1..43c505f98 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -19,6 +19,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@trycompai/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip'; import { parseRolesString } from '@/lib/permissions'; import type { Role } from '@db'; import { @@ -33,7 +34,7 @@ import { TableRow, Text, } from '@trycompai/design-system'; -import { Checkmark, Edit, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons'; +import { Checkmark, Edit, Information, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons'; import { toast } from 'sonner'; import { MultiRoleCombobox } from './MultiRoleCombobox'; @@ -52,7 +53,7 @@ interface MemberRowProps { isCurrentUserOwner: boolean; customRoles?: CustomRoleOption[]; taskCompletion?: TaskCompletion; - deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed'; + deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed'; isDeviceStatusLoading?: boolean; } @@ -87,6 +88,34 @@ function parseRoles(role: Role | Role[] | string): string[] { return parseRolesString(role); } +type DeviceStatus = 'compliant' | 'non-compliant' | 'stale' | 'not-installed'; + +function getDeviceStatusDotClass(status: DeviceStatus): string { + switch (status) { + case 'compliant': + return 'bg-green-500'; + case 'non-compliant': + return 'bg-yellow-500'; + case 'stale': + return 'bg-gray-400'; + case 'not-installed': + return 'bg-red-400'; + } +} + +function getDeviceStatusLabel(status: DeviceStatus): string { + switch (status) { + case 'compliant': + return 'Compliant'; + case 'non-compliant': + return 'Non-Compliant'; + case 'stale': + return 'Stale'; + case 'not-installed': + return 'Not Installed'; + } +} + export function MemberRow({ member, onRemove, @@ -251,27 +280,37 @@ export function MemberRow({ ) : (
- {deviceStatus === 'compliant' - ? 'Compliant' - : deviceStatus === 'non-compliant' - ? 'Non-Compliant' - : 'Not Installed'} + {getDeviceStatusLabel(deviceStatus)} + {deviceStatus === 'stale' && ( + + + + + + + This device's CompAI agent hasn't reported in recently, so we can't verify + its current compliance. Ask the employee to update or reinstall the agent. + + + + )}
)} diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.test.ts b/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.test.ts index 411512073..bcd67ac7d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.test.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.test.ts @@ -73,7 +73,7 @@ describe('computeDeviceStatusMap', () => { expect(map.mem_1).toBe('non-compliant'); }); - it('counts a stale device as non-compliant in the roll-up', () => { + it('returns stale when the only agent device is stale', () => { const map = computeDeviceStatusMap({ agentDevices: [ makeAgentDevice({ @@ -86,10 +86,10 @@ describe('computeDeviceStatusMap', () => { fleetHosts: [], complianceMemberIds: ['mem_1'], }); - expect(map.mem_1).toBe('non-compliant'); + expect(map.mem_1).toBe('stale'); }); - it('counts stale among mixed devices as non-compliant', () => { + it('returns stale when a compliant and a stale device are mixed', () => { const map = computeDeviceStatusMap({ agentDevices: [ makeAgentDevice({ memberId: 'mem_1', complianceStatus: 'compliant' }), @@ -102,9 +102,45 @@ describe('computeDeviceStatusMap', () => { fleetHosts: [], complianceMemberIds: ['mem_1'], }); + expect(map.mem_1).toBe('stale'); + }); + + it('prefers non-compliant over stale when both present', () => { + const map = computeDeviceStatusMap({ + agentDevices: [ + makeAgentDevice({ + memberId: 'mem_1', + complianceStatus: 'stale', + daysSinceLastCheckIn: 10, + }), + makeAgentDevice({ memberId: 'mem_1', complianceStatus: 'non_compliant' }), + ], + fleetHosts: [], + complianceMemberIds: ['mem_1'], + }); expect(map.mem_1).toBe('non-compliant'); }); + it('returns stale when all devices are stale', () => { + const map = computeDeviceStatusMap({ + agentDevices: [ + makeAgentDevice({ + memberId: 'mem_1', + complianceStatus: 'stale', + daysSinceLastCheckIn: 10, + }), + makeAgentDevice({ + memberId: 'mem_1', + complianceStatus: 'stale', + daysSinceLastCheckIn: 20, + }), + ], + fleetHosts: [], + complianceMemberIds: ['mem_1'], + }); + expect(map.mem_1).toBe('stale'); + }); + it('falls back to Fleet policy status when no agent device is present', () => { const map = computeDeviceStatusMap({ agentDevices: [], @@ -139,7 +175,48 @@ describe('computeDeviceStatusMap', () => { ], complianceMemberIds: ['mem_1'], }); - // Agent says stale → non-compliant, even if fleet would say compliant. + // Agent says stale → member is stale, even if fleet would say compliant. + expect(map.mem_1).toBe('stale'); + }); + + it('keeps a member non-compliant when a failing fleet host precedes a passing one', () => { + // Regression test for a fleet-loop last-host-wins bug: a passing host + // encountered after a failing one for the same member must not overwrite + // non-compliant with compliant. + const map = computeDeviceStatusMap({ + agentDevices: [], + fleetHosts: [ + makeFleetHost({ + member_id: 'mem_1', + policies: [{ id: 1, name: 'Disk Enc', response: 'fail' }], + }), + makeFleetHost({ + member_id: 'mem_1', + policies: [{ id: 2, name: 'Antivirus', response: 'pass' }], + }), + ], + complianceMemberIds: ['mem_1'], + }); + expect(map.mem_1).toBe('non-compliant'); + }); + + it('keeps a member non-compliant when a passing fleet host precedes a failing one', () => { + // Sibling order — the passing host runs first; the later failing host + // must still flip the member to non-compliant. + const map = computeDeviceStatusMap({ + agentDevices: [], + fleetHosts: [ + makeFleetHost({ + member_id: 'mem_1', + policies: [{ id: 1, name: 'Antivirus', response: 'pass' }], + }), + makeFleetHost({ + member_id: 'mem_1', + policies: [{ id: 2, name: 'Disk Enc', response: 'fail' }], + }), + ], + complianceMemberIds: ['mem_1'], + }); expect(map.mem_1).toBe('non-compliant'); }); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.ts b/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.ts index 629ad4745..a78a5282b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.ts +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/compute-device-status-map.ts @@ -1,17 +1,22 @@ import type { DeviceWithChecks, Host } from '../../devices/types'; -export type MemberDeviceStatus = 'compliant' | 'non-compliant' | 'not-installed'; +export type MemberDeviceStatus = 'compliant' | 'non-compliant' | 'stale' | 'not-installed'; /** * Roll-up per-member device compliance for the People table. * - * Rules (in order): + * Rules (in order of precedence): * 1. Every member in `complianceMemberIds` starts as `not-installed`. - * 2. For each agent device with a memberId in the set, ALL of that member's - * devices must have `complianceStatus === 'compliant'` to roll up to - * `compliant`. `non_compliant` and `stale` both count as non-compliant. + * 2. For each agent device with a memberId in the set, the member's roll-up is + * `non-compliant` > `stale` > `compliant`: + * - Any device with `complianceStatus === 'non_compliant'` → member is + * `'non-compliant'`. + * - Else any device with `complianceStatus === 'stale'` → member is + * `'stale'`. + * - Else (all devices compliant) → `'compliant'`. * 3. If a member has no agent device but has a Fleet host, we fall back to - * Fleet policy status. Agent data always wins when present. + * Fleet policy status (compliant / non-compliant; Fleet has no stale + * concept). Agent data always wins when present. */ export function computeDeviceStatusMap({ agentDevices, @@ -28,25 +33,38 @@ export function computeDeviceStatusMap({ map[id] = 'not-installed'; } - const agentComplianceByMember = new Map(); + const agentRollup = new Map(); for (const d of agentDevices) { if (!d.memberId || !complianceSet.has(d.memberId)) continue; - const prev = agentComplianceByMember.get(d.memberId); - // Stale devices count as non-compliant for the roll-up. - const isCompliant = d.complianceStatus === 'compliant'; - agentComplianceByMember.set(d.memberId, (prev ?? true) && isCompliant); + + const prev = agentRollup.get(d.memberId); + // Once a member has a non-compliant device, nothing can downgrade it. + if (prev === 'non-compliant') continue; + + if (d.complianceStatus === 'non_compliant') { + agentRollup.set(d.memberId, 'non-compliant'); + continue; + } + if (d.complianceStatus === 'stale') { + // Stale wins over compliant but loses to non-compliant. + if (prev !== 'stale') agentRollup.set(d.memberId, 'stale'); + continue; + } + // complianceStatus === 'compliant' (or any other benign value). + if (prev === undefined) agentRollup.set(d.memberId, 'compliant'); } - for (const [memberId, allCompliant] of agentComplianceByMember) { - map[memberId] = allCompliant ? 'compliant' : 'non-compliant'; + for (const [memberId, status] of agentRollup) { + map[memberId] = status; } for (const host of fleetHosts) { if (!host.member_id || !complianceSet.has(host.member_id)) continue; - if (agentComplianceByMember.has(host.member_id)) continue; + if (agentRollup.has(host.member_id)) continue; + // Non-compliant wins across multiple Fleet hosts for the same member — + // once we've seen a failing host, a later passing host must not clobber it. + if (map[host.member_id] === 'non-compliant') continue; const isCompliant = host.policies.every((p) => p.response === 'pass'); - if (map[host.member_id] !== 'non-compliant') { - map[host.member_id] = isCompliant ? 'compliant' : 'non-compliant'; - } + map[host.member_id] = isCompliant ? 'compliant' : 'non-compliant'; } return map; diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.test.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.test.tsx index dd948905c..8cec5285d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.test.tsx @@ -123,4 +123,58 @@ describe('DeviceAgentDevicesList', () => { expect(contents).toContain('Beta'); expect(contents).toContain('stale'); }); + + it('renders a stale-explainer tooltip trigger next to the Stale badge', () => { + render( + , + ); + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); + + it('renders the stale-explainer tooltip trigger when daysSinceLastCheckIn is null (never reported)', () => { + render( + , + ); + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a compliant device', () => { + render(); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a non-compliant device', () => { + render( + , + ); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); }); diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.tsx index 0b9c9861b..2b89923f0 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.tsx @@ -19,7 +19,8 @@ import { TableRow, Text, } from '@trycompai/design-system'; -import { Download, Search } from '@trycompai/design-system/icons'; +import { Download, Information, Search } from '@trycompai/design-system/icons'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { useMemo, useState } from 'react'; @@ -72,6 +73,12 @@ function staleLabel(daysSinceLastCheckIn: number | null): string { return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`; } +function staleTooltipCopy(daysSinceLastCheckIn: number | null): string { + return daysSinceLastCheckIn === null + ? "This device's CompAI agent hasn't reported any check-ins, so we can't verify its current compliance. Ask the employee to install or activate the agent." + : "This device's CompAI agent hasn't reported in over 7 days, so we can't verify its current compliance. Ask the employee to update or reinstall the agent."; +} + function UserNameCell({ device, orgId }: { device: DeviceWithChecks; orgId: string }) { const memberId = device.memberId; @@ -101,16 +108,26 @@ function UserNameCell({ device, orgId }: { device: DeviceWithChecks; orgId: stri function CompliantBadge({ device }: { device: DeviceWithChecks }) { if (device.complianceStatus === 'stale') { return ( - - {staleLabel(device.daysSinceLastCheckIn)} - +
+ {staleLabel(device.daysSinceLastCheckIn)} + + + + + + + {staleTooltipCopy(device.daysSinceLastCheckIn)} + + + +
); } if (device.complianceStatus === 'compliant') { diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.test.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.test.tsx index f79c8a674..2dbde9cc5 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.test.tsx @@ -85,18 +85,20 @@ describe('DeviceDetails compliance badge', () => { expect(screen.getByText('Stale')).toBeInTheDocument(); }); - it('sets stale badge title tooltip based on daysSinceLastCheckIn', () => { - const { rerender } = render( + it('renders a stale-explainer tooltip trigger for a stale device', () => { + render( , ); - expect(screen.getByText('Stale (30d)').closest('[title]')?.getAttribute('title')).toBe( - 'No check-in in 30 days', - ); + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); - rerender( + it('renders the stale-explainer tooltip trigger when daysSinceLastCheckIn is null (never reported)', () => { + render( { onClose={vi.fn()} />, ); - expect(screen.getByText('Stale').closest('[title]')?.getAttribute('title')).toBe( - 'No check-ins recorded', + expect( + screen.getByRole('button', { name: /What does Stale mean\?/i }), + ).toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a compliant device', () => { + render(); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); + }); + + it('does not render the stale-explainer tooltip trigger for a non-compliant device', () => { + render( + , ); + expect( + screen.queryByRole('button', { name: /What does Stale mean\?/i }), + ).not.toBeInTheDocument(); }); }); diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx index af5b350f8..9b2f1ce95 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx @@ -15,7 +15,8 @@ import { TableRow, Text, } from '@trycompai/design-system'; -import { ArrowLeft } from '@trycompai/design-system/icons'; +import { ArrowLeft, Information } from '@trycompai/design-system/icons'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip'; import type { DeviceWithChecks } from '../types'; const CHECK_FIELDS = [ @@ -42,18 +43,35 @@ function staleLabel(daysSinceLastCheckIn: number | null): string { return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`; } -function staleTitle(daysSinceLastCheckIn: number | null): string { +function staleTooltipCopy(daysSinceLastCheckIn: number | null): string { return daysSinceLastCheckIn === null - ? 'No check-ins recorded' - : `No check-in in ${daysSinceLastCheckIn} days`; + ? "This device's CompAI agent hasn't reported any check-ins, so we can't verify its current compliance. Ask the employee to install or activate the agent." + : "This device's CompAI agent hasn't reported in over 7 days, so we can't verify its current compliance. Ask the employee to update or reinstall the agent."; } function DeviceComplianceBadge({ device }: { device: DeviceWithChecks }) { if (device.complianceStatus === 'stale') { return ( - - {staleLabel(device.daysSinceLastCheckIn)} - +
+ {staleLabel(device.daysSinceLastCheckIn)} + + + + + + + {staleTooltipCopy(device.daysSinceLastCheckIn)} + + + +
); } if (device.complianceStatus === 'compliant') {