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') {