diff --git a/apps/api/trigger.config.ts b/apps/api/trigger.config.ts
index c700d132b1..8c6e1b7700 100644
--- a/apps/api/trigger.config.ts
+++ b/apps/api/trigger.config.ts
@@ -1,4 +1,3 @@
-import { syncVercelEnvVars } from '@trigger.dev/build/extensions/core';
import { defineConfig } from '@trigger.dev/sdk';
import { prismaExtension } from './customPrismaExtension';
import { emailExtension } from './emailExtension';
@@ -17,7 +16,6 @@ export default defineConfig({
}),
integrationPlatformExtension(),
emailExtension(),
- syncVercelEnvVars(),
],
},
retries: {
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
new file mode 100644
index 0000000000..ea25585a7f
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx
@@ -0,0 +1,180 @@
+import { render, screen } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { MemberWithUser } from './TeamMembers';
+
+// Mock next/navigation
+vi.mock('next/navigation', () => ({
+ useParams: () => ({ orgId: 'org_123' }),
+}));
+
+// Mock next/link
+vi.mock('next/link', () => ({
+ default: ({ children, href }: { children: React.ReactNode; href: string }) => (
+ {children}
+ ),
+}));
+
+// Mock sonner
+vi.mock('sonner', () => ({
+ toast: { success: vi.fn(), error: vi.fn() },
+}));
+
+// Mock child components that aren't relevant
+vi.mock('./MultiRoleCombobox', () => ({
+ MultiRoleCombobox: () => null,
+}));
+vi.mock('./RemoveDeviceAlert', () => ({
+ RemoveDeviceAlert: () => null,
+}));
+vi.mock('./RemoveMemberAlert', () => ({
+ RemoveMemberAlert: () => null,
+}));
+
+import { MemberRow } from './MemberRow';
+
+const baseMember = {
+ id: 'mem_1',
+ userId: 'usr_1',
+ organizationId: 'org_123',
+ role: 'employee',
+ department: null,
+ isActive: true,
+ deactivated: false,
+ fleetDmLabelId: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ user: {
+ id: 'usr_1',
+ name: 'Jane Doe',
+ email: 'jane@example.com',
+ emailVerified: true,
+ image: null,
+ role: 'user',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ banned: false,
+ banReason: null,
+ banExpires: null,
+ },
+} as unknown as MemberWithUser;
+
+const noop = vi.fn();
+
+function renderMemberRow(deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed') {
+ return render(
+
,
+ );
+}
+
+describe('MemberRow device status', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('shows "Not Installed" with red dot when deviceStatus is not-installed', () => {
+ renderMemberRow('not-installed');
+ expect(screen.getByText('Not Installed')).toBeInTheDocument();
+ expect(screen.getByText('Not Installed').className).toContain('text-muted-foreground');
+ });
+
+ it('shows "Not Installed" by default when deviceStatus is omitted', () => {
+ renderMemberRow();
+ expect(screen.getByText('Not Installed')).toBeInTheDocument();
+ });
+
+ it('shows "Compliant" with green dot when deviceStatus is compliant', () => {
+ renderMemberRow('compliant');
+ expect(screen.getByText('Compliant')).toBeInTheDocument();
+ expect(screen.getByText('Compliant').className).toContain('text-foreground');
+ });
+
+ it('shows "Non-Compliant" with yellow dot when deviceStatus is non-compliant', () => {
+ renderMemberRow('non-compliant');
+ expect(screen.getByText('Non-Compliant')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant').className).toContain('text-foreground');
+ });
+
+ it('does not show device status for platform admin', () => {
+ const adminMember = {
+ ...baseMember,
+ user: { ...baseMember.user, role: 'admin' as const },
+ } as MemberWithUser;
+
+ render(
+ ,
+ );
+
+ expect(screen.queryByText('Compliant')).not.toBeInTheDocument();
+ expect(screen.queryByText('Non-Compliant')).not.toBeInTheDocument();
+ expect(screen.queryByText('Not Installed')).not.toBeInTheDocument();
+ });
+
+ it('does not show device status for deactivated member', () => {
+ const deactivatedMember = {
+ ...baseMember,
+ deactivated: true,
+ } as MemberWithUser;
+
+ render(
+ ,
+ );
+
+ expect(screen.queryByText('Non-Compliant')).not.toBeInTheDocument();
+ expect(screen.queryByText('Compliant')).not.toBeInTheDocument();
+ });
+
+ it('renders correct dot colors for each status', () => {
+ const { container, unmount } = renderMemberRow('compliant');
+ const greenDot = container.querySelector('.bg-green-500');
+ expect(greenDot).toBeInTheDocument();
+ unmount();
+
+ const { container: c2, unmount: u2 } = renderMemberRow('non-compliant');
+ const yellowDot = c2.querySelector('.bg-yellow-500');
+ expect(yellowDot).toBeInTheDocument();
+ u2();
+
+ const { container: c3 } = renderMemberRow('not-installed');
+ const redDot = c3.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 cc041cd4be..72fef1ac15 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
@@ -51,7 +51,7 @@ interface MemberRowProps {
isCurrentUserOwner: boolean;
customRoles?: CustomRoleOption[];
taskCompletion?: TaskCompletion;
- hasDeviceAgentDevice?: boolean;
+ deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed';
}
function getInitials(name?: string | null, email?: string | null): string {
@@ -95,7 +95,7 @@ export function MemberRow({
isCurrentUserOwner,
customRoles = [],
taskCompletion,
- hasDeviceAgentDevice,
+ deviceStatus = 'not-installed',
}: MemberRowProps) {
const { orgId } = useParams<{ orgId: string }>();
@@ -231,7 +231,7 @@ export function MemberRow({
- {/* AGENT */}
+ {/* DEVICE */}
{isPlatformAdmin || isDeactivated ? (
@@ -241,11 +241,25 @@ export function MemberRow({
-
- {hasDeviceAgentDevice ? 'Installed' : 'Not Installed'}
+
+ {deviceStatus === 'compliant'
+ ? 'Compliant'
+ : deviceStatus === 'non-compliant'
+ ? 'Non-Compliant'
+ : 'Not Installed'}
)}
@@ -307,7 +321,7 @@ export function MemberRow({
)}
{!isDeactivated &&
- (member.fleetDmLabelId || hasDeviceAgentDevice) &&
+ (member.fleetDmLabelId || deviceStatus !== 'not-installed') &&
isCurrentUserOwner && (
{
diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx
index 8fa34be45d..67c36a96ad 100644
--- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx
@@ -24,16 +24,19 @@ export interface TaskCompletion {
hipaa?: { completed: number; total: number };
}
+export type DeviceStatus = 'compliant' | 'non-compliant' | 'not-installed';
+
export interface TeamMembersProps {
canManageMembers: boolean;
canInviteUsers: boolean;
isAuditor: boolean;
isCurrentUserOwner: boolean;
organizationId: string;
+ deviceStatusMap: Record;
}
export async function TeamMembers(props: TeamMembersProps) {
- const { canManageMembers, canInviteUsers, isAuditor, isCurrentUserOwner, organizationId } = props;
+ const { canManageMembers, canInviteUsers, isAuditor, isCurrentUserOwner, organizationId, deviceStatusMap } = props;
if (!organizationId) {
return null;
@@ -64,19 +67,6 @@ export async function TeamMembers(props: TeamMembersProps) {
const employeeMembers = await filterComplianceMembers(members, organizationId);
- // Build a set of member IDs that have device-agent devices
- const memberIds = members.map((m) => m.id);
- const devicesForMembers = await db.device.findMany({
- where: {
- organizationId,
- memberId: { in: memberIds },
- },
- select: { memberId: true },
- });
- const memberIdsWithDeviceAgent = [
- ...new Set(devicesForMembers.map((d) => d.memberId)),
- ];
-
if (employeeMembers.length > 0) {
const [org, hipaaInstance] = await Promise.all([
db.organization.findUnique({
@@ -156,7 +146,7 @@ export async function TeamMembers(props: TeamMembersProps) {
isCurrentUserOwner={isCurrentUserOwner}
employeeSyncData={employeeSyncData}
taskCompletionMap={taskCompletionMap}
- memberIdsWithDeviceAgent={memberIdsWithDeviceAgent}
+ deviceStatusMap={deviceStatusMap}
/>
);
}
diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx
index f0e6268373..64d62ef879 100644
--- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx
@@ -53,7 +53,7 @@ interface TeamMembersClientProps {
isCurrentUserOwner: boolean;
employeeSyncData: EmployeeSyncConnectionsData;
taskCompletionMap: Record;
- memberIdsWithDeviceAgent: string[];
+ deviceStatusMap: Record;
}
export function TeamMembersClient({
@@ -65,7 +65,7 @@ export function TeamMembersClient({
isCurrentUserOwner,
employeeSyncData,
taskCompletionMap,
- memberIdsWithDeviceAgent,
+ deviceStatusMap,
}: TeamMembersClientProps) {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');
@@ -470,9 +470,7 @@ export function TeamMembersClient({
isCurrentUserOwner={isCurrentUserOwner}
customRoles={customRoles}
taskCompletion={taskCompletionMap[(item as MemberWithUser).id]}
- hasDeviceAgentDevice={memberIdsWithDeviceAgent.includes(
- (item as MemberWithUser).id,
- )}
+ deviceStatus={deviceStatusMap[(item as MemberWithUser).id] ?? 'not-installed'}
/>
) : (
({
+ PieChart: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ Pie: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ Cell: () => ,
+ Label: () => null,
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+// Mock @trycompai/ui chart components
+vi.mock('@trycompai/ui/chart', () => ({
+ ChartContainer: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ ChartTooltip: () => null,
+ ChartTooltipContent: () => null,
+}));
+
+import { DeviceComplianceChart } from './DeviceComplianceChart';
+
+function makeAgentDevice(overrides: Partial = {}): DeviceWithChecks {
+ return {
+ id: `dev_${Math.random().toString(36).slice(2)}`,
+ name: 'MacBook Pro',
+ hostname: 'macbook-pro',
+ platform: 'macos',
+ osVersion: '14.0',
+ serialNumber: 'SN123',
+ hardwareModel: 'MacBookPro18,1',
+ isCompliant: true,
+ diskEncryptionEnabled: true,
+ antivirusEnabled: true,
+ passwordPolicySet: true,
+ screenLockEnabled: true,
+ checkDetails: null,
+ lastCheckIn: new Date().toISOString(),
+ agentVersion: '1.0.0',
+ installedAt: new Date().toISOString(),
+ user: { name: 'Test User', email: 'test@example.com' },
+ source: 'device_agent',
+ ...overrides,
+ };
+}
+
+function makeFleetDevice(overrides: Partial = {}): Host {
+ return {
+ id: Math.floor(Math.random() * 10000),
+ hostname: 'fleet-host',
+ computer_name: 'Fleet Device',
+ platform: 'darwin',
+ os_version: '14.0',
+ hardware_serial: 'FSN123',
+ hardware_model: 'MacBookPro18,1',
+ status: 'online',
+ seen_time: new Date().toISOString(),
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ disk_encryption_enabled: true,
+ display_name: 'Fleet Device',
+ display_text: 'Fleet Device',
+ software: [],
+ software_updated_at: '',
+ detail_updated_at: '',
+ label_updated_at: '',
+ policy_updated_at: '',
+ last_enrolled_at: '',
+ refetch_requested: false,
+ uuid: '',
+ osquery_version: '',
+ orbit_version: '',
+ fleet_desktop_version: '',
+ scripts_enabled: false,
+ build: '',
+ platform_like: '',
+ code_name: '',
+ uptime: 0,
+ memory: 0,
+ cpu_type: '',
+ cpu_subtype: '',
+ cpu_brand: '',
+ cpu_physical_cores: 0,
+ cpu_logical_cores: 0,
+ hardware_vendor: '',
+ hardware_version: '',
+ public_ip: '',
+ primary_ip: '',
+ primary_mac: '',
+ distributed_interval: 0,
+ config_tls_refresh: 0,
+ logger_tls_period: 0,
+ team_id: null,
+ pack_stats: [],
+ team_name: null,
+ users: [],
+ gigs_disk_space_available: 0,
+ percent_disk_space_available: 0,
+ gigs_total_disk_space: 0,
+ issues: {},
+ mdm: { connected_to_fleet: false, dep_profile_error: false, encryption_key_available: false, enrollment_status: '' },
+ refetch_critical_queries_until: null,
+ last_restarted_at: '',
+ labels: [],
+ packs: [],
+ batteries: [],
+ end_users: [],
+ last_mdm_enrolled_at: '',
+ last_mdm_checked_in_at: '',
+ policies: [{ id: 1, name: 'Disk Encryption', response: 'pass', attachments: [] }],
+ ...overrides,
+ } as Host;
+}
+
+describe('DeviceComplianceChart', () => {
+ it('shows empty state when no devices from either source', () => {
+ render();
+ expect(screen.getByText('No device data available. Please make sure your employees access the portal and install the device agent.')).toBeInTheDocument();
+ });
+
+ it('counts agent devices correctly — all compliant', () => {
+ const devices = [
+ makeAgentDevice({ isCompliant: true }),
+ makeAgentDevice({ isCompliant: true }),
+ ];
+
+ render();
+ expect(screen.getByText('Compliant (2)')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant (0)')).toBeInTheDocument();
+ });
+
+ it('counts agent devices correctly — mixed compliance', () => {
+ const devices = [
+ makeAgentDevice({ isCompliant: true }),
+ makeAgentDevice({ isCompliant: false }),
+ ];
+
+ render();
+ expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant (1)')).toBeInTheDocument();
+ });
+
+ it('counts fleet devices correctly — all policies pass', () => {
+ const devices = [
+ makeFleetDevice({
+ policies: [
+ { id: 1, name: 'Encryption', response: 'pass' },
+ { id: 2, name: 'Antivirus', response: 'pass' },
+ ],
+ }),
+ ];
+
+ render();
+ expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant (0)')).toBeInTheDocument();
+ });
+
+ it('counts fleet devices correctly — some policies fail', () => {
+ const devices = [
+ makeFleetDevice({
+ policies: [
+ { id: 1, name: 'Encryption', response: 'pass' },
+ { id: 2, name: 'Antivirus', response: 'fail' },
+ ],
+ }),
+ ];
+
+ render();
+ expect(screen.getByText('Compliant (0)')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant (1)')).toBeInTheDocument();
+ });
+
+ it('combines agent and fleet devices in total count', () => {
+ const agentDevices = [
+ makeAgentDevice({ isCompliant: true }),
+ makeAgentDevice({ isCompliant: false }),
+ ];
+ const fleetDevices = [
+ makeFleetDevice({
+ policies: [{ id: 1, name: 'Encryption', response: 'pass' }],
+ }),
+ ];
+
+ render();
+ // 1 compliant agent + 1 compliant fleet = 2, 1 non-compliant agent = 1
+ expect(screen.getByText('Compliant (2)')).toBeInTheDocument();
+ expect(screen.getByText('Non-Compliant (1)')).toBeInTheDocument();
+ });
+
+ it('treats fleet device with no policies as compliant', () => {
+ const devices = [makeFleetDevice({ policies: [] })];
+
+ render();
+ // Array.every on empty array returns true
+ expect(screen.getByText('Compliant (1)')).toBeInTheDocument();
+ });
+});
diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx
index 827f6a4272..c9eeb4d0d5 100644
--- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx
@@ -9,10 +9,11 @@ import {
} from '@trycompai/ui/chart';
import * as React from 'react';
import { Cell, Label, Pie, PieChart } from 'recharts';
-import type { Host } from '../types';
+import type { DeviceWithChecks, Host } from '../types';
interface DeviceComplianceChartProps {
- devices: Host[];
+ fleetDevices: Host[];
+ agentDevices: DeviceWithChecks[];
}
const CHART_COLORS = {
@@ -20,15 +21,27 @@ const CHART_COLORS = {
nonCompliant: 'hsl(var(--chart-destructive))',
};
-export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) {
+export function DeviceComplianceChart({ fleetDevices, agentDevices }: DeviceComplianceChartProps) {
+ const devices = [...(agentDevices ?? []), ...(fleetDevices ?? [])];
+
const { pieDisplayData, legendDisplayData } = React.useMemo(() => {
- if (!devices || devices.length === 0) {
+ if (devices.length === 0) {
return { pieDisplayData: [], legendDisplayData: [] };
}
let compliantCount = 0;
let nonCompliantCount = 0;
- for (const device of devices) {
+ // Count device-agent devices
+ for (const device of agentDevices ?? []) {
+ if (device.isCompliant) {
+ compliantCount++;
+ } else {
+ nonCompliantCount++;
+ }
+ }
+
+ // Count fleet devices
+ for (const device of fleetDevices ?? []) {
const isCompliant = device.policies.every((policy) => policy.response === 'pass');
if (isCompliant) {
compliantCount++;
@@ -36,6 +49,7 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) {
nonCompliantCount++;
}
}
+
const allItems = [
{
name: 'Compliant',
@@ -52,11 +66,9 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) {
pieDisplayData: allItems.filter((item) => item.value > 0),
legendDisplayData: allItems,
};
- }, [devices]);
+ }, [agentDevices, fleetDevices]);
- const totalDevices = React.useMemo(() => {
- return devices?.length || 0;
- }, [devices]);
+ const totalDevices = devices.length;
const chartConfig = {
devices: {
diff --git a/apps/app/src/app/(app)/[orgId]/people/page.tsx b/apps/app/src/app/(app)/[orgId]/people/page.tsx
index 6a2fcb9cf5..0bbbe94c1b 100644
--- a/apps/app/src/app/(app)/[orgId]/people/page.tsx
+++ b/apps/app/src/app/(app)/[orgId]/people/page.tsx
@@ -155,6 +155,34 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId:
(host) => !host.member_id || !memberIdsWithAgent.has(host.member_id),
);
+ // Build unified device status map from the SAME data both tabs use.
+ // This ensures the member list and compliance chart agree on compliance.
+ const deviceStatusMap: Record = {};
+
+ // Device-agent devices: compliant only if ALL of a member's devices pass
+ const agentComplianceByMember = new Map();
+ for (const d of agentDevices) {
+ if (!d.memberId) continue;
+ const prev = agentComplianceByMember.get(d.memberId);
+ agentComplianceByMember.set(d.memberId, (prev ?? true) && d.isCompliant);
+ }
+ for (const [memberId, allCompliant] of agentComplianceByMember) {
+ deviceStatusMap[memberId] = allCompliant ? 'compliant' : 'non-compliant';
+ }
+
+ // Fleet-only devices: use the same merged policy data the chart uses
+ // (Fleet API automated checks + DB manual overrides, already combined by getFleetHosts)
+ for (const host of filteredFleetDevices) {
+ if (!host.member_id) continue;
+ // If already set by device-agent, skip (agent takes priority)
+ if (agentComplianceByMember.has(host.member_id)) continue;
+ const isCompliant = host.policies.every((p) => p.response === 'pass');
+ // If multiple fleet devices for same member, non-compliant if ANY device fails
+ if (!isCompliant || !deviceStatusMap[host.member_id]) {
+ deviceStatusMap[host.member_id] = isCompliant ? 'compliant' : 'non-compliant';
+ }
+ }
+
return (
}
employeeTasksContent={showEmployeeTasks ? : null}
devicesContent={
+ {/* Unified compliance chart covering both device-agent and fleet devices */}
+
+
{/* Device Agent devices (new system) */}
{agentDevices.length > 0 && (
)}
- {/* Fleet devices (legacy) — shown exactly as main branch */}
-
-
+ {/* Fleet devices (legacy) — only for members without the newer device agent */}
+ {filteredFleetDevices.length > 0 && (
+
+ )}
}
orgChartContent={
diff --git a/apps/app/trigger.config.ts b/apps/app/trigger.config.ts
index fb66682b30..38626c313e 100644
--- a/apps/app/trigger.config.ts
+++ b/apps/app/trigger.config.ts
@@ -1,5 +1,4 @@
import { PrismaInstrumentation } from '@prisma/instrumentation';
-import { syncVercelEnvVars } from '@trigger.dev/build/extensions/core';
import { puppeteer } from '@trigger.dev/build/extensions/puppeteer';
import { defineConfig } from '@trigger.dev/sdk';
import { prismaExtension } from './customPrismaExtension';
@@ -17,7 +16,6 @@ export default defineConfig({
dbPackageVersion: '^2.0.0',
}),
puppeteer(),
- syncVercelEnvVars(),
],
},
retries: {
diff --git a/packages/email/emails/all-policy-notification.tsx b/packages/email/emails/all-policy-notification.tsx
index 1472d65ef6..d794bc9014 100644
--- a/packages/email/emails/all-policy-notification.tsx
+++ b/packages/email/emails/all-policy-notification.tsx
@@ -2,7 +2,6 @@ import {
Body,
Button,
Container,
- Font,
Heading,
Html,
Link,
@@ -35,31 +34,7 @@ export const AllPolicyNotificationEmail = ({
return (
-
-
-
-
-
-
- {subjectText}
+{subjectText}
-
-
-
-
-
-
- You've been invited to the Comp AI Portal
+You've been invited to the Comp AI Portal
{
return (
-
-
-
-
-
- You've been invited to join Comp AI
+You've been invited to join Comp AI
{
return (
-
-
-
-
-
- Login Link for Comp AI
+Login Link for Comp AI
{
return (
-
-
-
-
-
-
- Get started with Comp AI
+Get started with Comp AI
{
return (
-
-
-
-
-
-
- One-Time Password for Comp AI
+One-Time Password for Comp AI
-
-
-
-
-
-
- {subjectText}
+{subjectText}
return (
-
-
-
-
-
-
- Comp AI - Task Reminder
+Comp AI - Task Reminder
-
-
-
-
-
-
-
+
Task "{taskName}" {statusLabel} - {organizationName}
diff --git a/packages/email/emails/reminders/weekly-task-digest.tsx b/packages/email/emails/reminders/weekly-task-digest.tsx
index aa90fe17f5..7f1f18f942 100644
--- a/packages/email/emails/reminders/weekly-task-digest.tsx
+++ b/packages/email/emails/reminders/weekly-task-digest.tsx
@@ -2,7 +2,6 @@ import {
Body,
Button,
Container,
- Font,
Heading,
Html,
Link,
@@ -46,31 +45,7 @@ export const WeeklyTaskDigestEmail = ({
return (
-
-
-
-
-
-
- {taskCountMessage}
+{taskCountMessage}
-
-
-
-
-
-
- Congratulations! You've completed your Security Awareness Training
+Congratulations! You've completed your Security Awareness Training
-
-
-
-
-
-
- Member removed - items require reassignment
+Member removed - items require reassignment
{
- const confirmationUrl = `https://trycomp.ai/api/waitlist?email=${email}`;
-
- return (
-
-
-
-
-
-
-
- Confirm your email to join the Comp AI waitlist
-
-
-
-
-
- Just one more step
-
-
-
- To claim your spot on the Comp AI waitlist, please confirm your email.
-
-
-
-
- or copy and paste this URL into your browser{' '}
-
- {confirmationUrl}
-
-
-
-
-
-
- This email was intended for{' '}
- {email}. If you did not request
- this, please ignore this email.
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default WaitlistEmail;
diff --git a/packages/email/index.ts b/packages/email/index.ts
index 4b806025e9..bfe30b8a5d 100644
--- a/packages/email/index.ts
+++ b/packages/email/index.ts
@@ -9,7 +9,6 @@ export * from './emails/policy-notification';
export * from './emails/reminders/task-status-notification';
export * from './emails/training-completed';
export * from './emails/unassigned-items-notification';
-export * from './emails/waitlist';
// Email sending functions
export * from './lib/all-policy-notification';
@@ -21,5 +20,4 @@ export * from './lib/resend';
export * from './lib/training-completed';
export * from './lib/unassigned-items-notification';
export * from './lib/unsubscribe';
-export * from './lib/waitlist';
export * from './lib/weekly-task-digest';
diff --git a/packages/email/lib/waitlist.ts b/packages/email/lib/waitlist.ts
deleted file mode 100644
index 0b52a78134..0000000000
--- a/packages/email/lib/waitlist.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { WaitlistEmail } from '../emails/waitlist';
-import { sendEmail } from './resend';
-
-export const sendWaitlistEmail = async (params: { email: string }) => {
- const { email } = params;
-
- try {
- const sent = await sendEmail({
- to: email,
- subject: 'Welcome to the Comp AI waitlist!',
- react: WaitlistEmail({ email }),
- });
-
- if (!sent) {
- console.error('Failed to send waitlist email');
- return { success: false };
- }
-
- return { success: true };
- } catch (error) {
- console.error('Error sending waitlist email:', error);
- return { success: false };
- }
-};