- Click the download button below to get the Device Agent installer.
-
-
- {isMacOS && !hasInstalledAgent && (
-
-
+ {agentDevices.length > 0 && (
+
+
+ Your Devices
+
+ {agentDevices.map((device) => (
+
+
+
+ {device.name}
+
+
+
+
+
+ {device.isCompliant ? (
+
+ ) : (
+
+ )}
+
+ {device.isCompliant
+ ? 'All security checks passing'
+ : 'Some security checks need attention'}
+
- )}
-
-
-
-
- Install the Comp AI Device Agent
-
- {isMacOS
- ? 'Double-click the downloaded DMG file and follow the installation instructions.'
- : detectedOS === 'linux'
- ? 'Install the downloaded DEB package using your package manager or by double-clicking it.'
- : 'Double-click the downloaded EXE file and follow the installation instructions.'}
-
-
-
- Login with your work email
-
- After installation, login with your work email, select your organization and
- then click "Link Device".
-
-
-
+
+ {device.platform} · {device.osVersion}
+ {device.lastCheckIn && (
+ <> · Last check-in: {new Date(device.lastCheckIn).toLocaleDateString()}>
+ )}
+
+
+
+
+ ))}
- ) : hasAgentDevice ? (
-
-
-
- {agentDevice.name}
-
-
-
-
-
- {agentDevice.isCompliant ? (
-
- ) : (
-
- )}
-
- {agentDevice.isCompliant
- ? 'All security checks passing'
- : 'Some security checks need attention'}
-
-
-
- {agentDevice.platform} · {agentDevice.osVersion}
- {agentDevice.lastCheckIn && (
- <> · Last check-in: {new Date(agentDevice.lastCheckIn).toLocaleDateString()}>
- )}
-
-
-
-
- ) : hasFleetDevice ? (
+ )}
+
+ {!hasAnyAgentDevice && hasFleetDevice && (
@@ -308,7 +265,59 @@ export function DeviceAgentAccordionItem({
- ) : null}
+ )}
+
+
+
+ {hasAnyAgentDevice ? 'Add Another Device' : 'Install on a Device'}
+
+
+ -
+ Download the Device Agent installer.
+
+ Click the download button below to get the Device Agent installer.
+
+
+ {isMacOS && (
+
+
+
+ )}
+
+
+
+ -
+ Install the Comp AI Device Agent
+
+ {isMacOS
+ ? 'Double-click the downloaded DMG file and follow the installation instructions.'
+ : detectedOS === 'linux'
+ ? 'Install the downloaded DEB package using your package manager or by double-clicking it.'
+ : 'Double-click the downloaded EXE file and follow the installation instructions.'}
+
+
+ -
+ Login with your work email
+
+ After installation, login with your work email, select your organization and
+ then click "Link Device".
+
+
+
+
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/HipaaTrainingAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/HipaaTrainingAccordionItem.tsx
new file mode 100644
index 0000000000..bf175b64f6
--- /dev/null
+++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/HipaaTrainingAccordionItem.tsx
@@ -0,0 +1,285 @@
+'use client';
+
+import {
+ HIPAA_TRAINING_ID,
+ hipaaAcknowledgements,
+ hipaaTrainingSections,
+} from '@/lib/data/hipaa-training-content';
+import { useTrainingCompletions } from '@/hooks/use-training-completions';
+import {
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+ Button,
+ cn,
+} from '@trycompai/design-system';
+import { CheckmarkFilled, CircleDash } from '@trycompai/design-system/icons';
+import { useState } from 'react';
+
+export function HipaaTrainingAccordionItem() {
+ const { completions, markVideoComplete } = useTrainingCompletions();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [allChecked, setAllChecked] = useState(false);
+ const [checkedItems, setCheckedItems] = useState>({});
+
+ const hipaaCompletion = completions.find(
+ (c) => c.videoId === HIPAA_TRAINING_ID && c.completedAt !== null,
+ );
+ const isCompleted = !!hipaaCompletion;
+
+ const handleCheckChange = (index: number, checked: boolean) => {
+ const updated = { ...checkedItems, [index]: checked };
+ setCheckedItems(updated);
+ setAllChecked(
+ hipaaAcknowledgements.every((_, i) => updated[i] === true),
+ );
+ };
+
+ const handleAcknowledge = async () => {
+ if (!allChecked || isSubmitting) return;
+ setIsSubmitting(true);
+ try {
+ await markVideoComplete(HIPAA_TRAINING_ID);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {isCompleted ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ HIPAA Security Awareness Training
+
+
+
+
+
+
+
+ Read the following HIPAA security awareness training and
+ acknowledge each statement at the bottom to complete this
+ requirement.
+
+
+ {isCompleted ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ );
+}
+
+function CompletedBanner({ completedAt }: { completedAt: Date | null }) {
+ return (
+
+
+
+
+
+
+ HIPAA training acknowledged
+
+
+ {completedAt
+ ? `Completed on ${new Date(completedAt).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}. A certificate was emailed to you.`
+ : 'You have completed the HIPAA Security Awareness Training.'}
+
+
+
+ );
+}
+
+function TrainingContent() {
+ return (
+
+ {hipaaTrainingSections.map((section) => (
+
+
{section.title}
+
+
+ ))}
+
+ );
+}
+
+function TrainingSectionContent({ content }: { content: string }) {
+ const lines = content.split('\n');
+ const elements: React.ReactNode[] = [];
+ let tableRows: string[][] = [];
+ let inTable = false;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const trimmed = line.trim();
+
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
+ if (trimmed.replace(/[|\-\s]/g, '') === '') continue;
+ inTable = true;
+ const cells = trimmed
+ .split('|')
+ .slice(1, -1)
+ .map((c) => c.trim());
+ tableRows.push(cells);
+ continue;
+ }
+
+ if (inTable) {
+ elements.push(
+ ,
+ );
+ tableRows = [];
+ inTable = false;
+ }
+
+ if (trimmed.startsWith('**') && trimmed.endsWith('**')) {
+ elements.push(
+
+ {trimmed.slice(2, -2)}
+
,
+ );
+ } else if (trimmed.startsWith('- ')) {
+ elements.push(
+
+ {trimmed.slice(2)}
+ ,
+ );
+ } else if (trimmed) {
+ elements.push(
+
+ {trimmed}
+
,
+ );
+ }
+ }
+
+ if (inTable && tableRows.length > 0) {
+ elements.push(
+ ,
+ );
+ }
+
+ return <>{elements}>;
+}
+
+function TrainingTable({ rows }: { rows: string[][] }) {
+ if (rows.length === 0) return null;
+ const [header, ...body] = rows;
+
+ return (
+
+
+
+
+ {header.map((cell, i) => (
+ |
+ {cell}
+ |
+ ))}
+
+
+
+ {body.map((row, ri) => (
+
+ {row.map((cell, ci) => (
+ |
+ {cell}
+ |
+ ))}
+
+ ))}
+
+
+
+ );
+}
+
+function AcknowledgementSection({
+ checkedItems,
+ onCheckChange,
+ allChecked,
+ isSubmitting,
+ onAcknowledge,
+}: {
+ checkedItems: Record;
+ onCheckChange: (index: number, checked: boolean) => void;
+ allChecked: boolean;
+ isSubmitting: boolean;
+ onAcknowledge: () => void;
+}) {
+ return (
+
+
Acknowledgement
+
+ By acknowledging this training, you confirm the following:
+
+
+ {hipaaAcknowledgements.map((statement, index) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx
index 2575029f3b..0fcb15bfde 100644
--- a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx
+++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx
@@ -105,16 +105,23 @@ export function PoliciesAccordionItem({ policies, member }: PoliciesAccordionIte
);
})}
-
+
+
+ {hasAcceptedPolicies && (
+
+
+
+ )}
+
>
) : (
No policies ready to be signed.
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
index 4e71a2eb6e..967d7cdd41 100644
--- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
+++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx
@@ -57,8 +57,8 @@ export default async function OrganizationPage({ params }: { params: Promise<{ o
// Fleet policies - only fetch if member has a fleet device label
const fleetData = await getFleetPolicies(member);
- // Device agent device - fetch from DB
- const agentDevice = await db.device.findFirst({
+ // Device agent devices - fetch all for this member
+ const agentDevices = await db.device.findMany({
where: {
memberId: member.id,
organizationId: orgId,
@@ -75,7 +75,7 @@ export default async function OrganizationPage({ params }: { params: Promise<{ o
member={member}
fleetPolicies={fleetData.fleetPolicies}
host={fleetData.device}
- agentDevice={agentDevice}
+ agentDevices={agentDevices}
/>
);
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/policies/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/policies/page.tsx
new file mode 100644
index 0000000000..a17f8f82f2
--- /dev/null
+++ b/apps/portal/src/app/(app)/(home)/[orgId]/policies/page.tsx
@@ -0,0 +1,116 @@
+import { auth } from '@/app/lib/auth';
+import { db } from '@db/server';
+import {
+ Breadcrumb,
+ Card,
+ CardContent,
+ PageHeader,
+ PageLayout,
+ Stack,
+ Text,
+} from '@trycompai/design-system';
+import { Document } from '@trycompai/design-system/icons';
+import { headers } from 'next/headers';
+import Link from 'next/link';
+import { redirect } from 'next/navigation';
+
+export default async function SignedPoliciesPage({
+ params,
+}: {
+ params: Promise<{ orgId: string }>;
+}) {
+ const { orgId } = await params;
+
+ const session = await auth.api.getSession({
+ headers: await headers(),
+ });
+
+ if (!session?.user) {
+ redirect('/auth');
+ }
+
+ const member = await db.member.findFirst({
+ where: {
+ userId: session.user.id,
+ organizationId: orgId,
+ deactivated: false,
+ },
+ });
+
+ if (!member) {
+ redirect('/');
+ }
+
+ const policies = await db.policy.findMany({
+ where: {
+ organizationId: orgId,
+ status: 'published',
+ isRequiredToSign: true,
+ isArchived: false,
+ signedBy: { has: member.id },
+ },
+ orderBy: { name: 'asc' },
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ updatedAt: true,
+ },
+ });
+
+ return (
+
+ },
+ },
+ { label: 'Signed Policies', isCurrent: true },
+ ]}
+ />
+
+
+ {policies.length === 0 ? (
+
+ No signed policies yet.
+
+ ) : (
+
+ {policies.map((policy) => (
+
+
+
+
+
+
+
+
+
+ {policy.name}
+
+ {policy.description && (
+
+ {policy.description}
+
+ )}
+
+
+ {new Date(policy.updatedAt).toLocaleDateString()}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/portal/src/app/components/user-menu-client.tsx b/apps/portal/src/app/components/user-menu-client.tsx
new file mode 100644
index 0000000000..a02223ff49
--- /dev/null
+++ b/apps/portal/src/app/components/user-menu-client.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+import { Avatar, AvatarFallback, AvatarImage } from '@trycompai/ui/avatar';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@trycompai/ui/dropdown-menu';
+import { Logout } from './logout';
+
+type UserMenuClientProps = {
+ name: string | null;
+ email: string | null;
+ image: string | null;
+ userInitials: string;
+};
+
+export function UserMenuClient({ name, email, image, userInitials }: UserMenuClientProps) {
+ return (
+
+
+
+ {image ? : null}
+
+ {userInitials}
+
+
+
+
+
+
+
+ {name}
+ {email}
+
+
Beta
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/app/components/user-menu.tsx b/apps/portal/src/app/components/user-menu.tsx
index 17185ca85d..b95938dfa7 100644
--- a/apps/portal/src/app/components/user-menu.tsx
+++ b/apps/portal/src/app/components/user-menu.tsx
@@ -1,14 +1,6 @@
import { auth } from '@/app/lib/auth';
-import { Avatar, AvatarFallback, AvatarImageNext } from '@trycompai/ui/avatar';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@trycompai/ui/dropdown-menu';
import { headers } from 'next/headers';
-import { Logout } from './logout';
+import { UserMenuClient } from './user-menu-client';
// Helper function to get initials
function getInitials(name?: string | null, email?: string | null): string {
@@ -33,44 +25,19 @@ export async function UserMenu() {
headers: await headers(),
});
- const userInitials = getInitials(session?.user?.name, session?.user?.email);
+ if (!session?.user) {
+ return null;
+ }
+
+ const user = session.user;
+ const userInitials = getInitials(user.name, user.email);
return (
-
-
-
- {session?.user?.image && (
-
- )}
-
- {userInitials}
-
-
-
-
- {' '}
-
-
-
-
- {session?.user?.name}
-
-
- {session?.user?.email}
-
-
-
Beta
-
-
-
-
-
-
+
);
}
diff --git a/apps/portal/src/lib/data/hipaa-training-content.ts b/apps/portal/src/lib/data/hipaa-training-content.ts
new file mode 100644
index 0000000000..982c98dc54
--- /dev/null
+++ b/apps/portal/src/lib/data/hipaa-training-content.ts
@@ -0,0 +1,67 @@
+export const HIPAA_TRAINING_ID = 'hipaa-sat-1';
+
+export interface HipaaTrainingSection {
+ title: string;
+ content: string;
+}
+
+export const hipaaTrainingSections: readonly HipaaTrainingSection[] = [
+ {
+ title: '1. Why this training exists',
+ content: `This document supplements the organization's existing general security awareness training with HIPAA-specific expectations for protecting Protected Health Information (PHI), including electronic PHI (ePHI), paper records, and verbal disclosures.
+
+It is intended to support onboarding and annual refresher requirements and to provide clear evidence that personnel were informed of their HIPAA security and privacy responsibilities.`,
+ },
+ {
+ title: '2. What employees must understand',
+ content: `- PHI may exist in systems, email, chat, files, paper documents, screenshots, voicemail, and verbal conversations.
+- Access to PHI is permitted only for authorized job duties and only to the minimum extent necessary to perform those duties.
+- PHI must not be sent, stored, printed, discussed, or shared using unapproved methods or with unauthorized people.
+- Security incidents, suspected misdirected disclosures, phishing attempts, lost devices, and any possible PHI exposure must be reported immediately.`,
+ },
+ {
+ title: '3. Required day-to-day behaviors',
+ content: `**Protect PHI in all forms**
+- Do not leave records, labels, printouts, or screens containing PHI unattended.
+- Verify recipient names, addresses, and fax numbers before sending information.
+- Use only approved storage locations, applications, and workflows for PHI.
+
+**Email, messaging, and file sharing**
+- Use approved encrypted or otherwise authorized methods when transmitting PHI.
+- Do not auto-forward work email containing PHI to personal accounts.
+- Do not copy PHI into public AI tools or non-approved cloud services.
+
+**Passwords, access, and devices**
+- Use unique passwords, enable multi-factor authentication where required, and never share credentials.
+- Lock your screen when you step away and secure laptops and mobile devices physically.
+- Report lost or stolen devices immediately, even if you are unsure whether PHI was involved.
+
+**Phishing and social engineering**
+- Treat urgent requests, payment changes, unusual login prompts, and requests for credentials or patient information as suspicious until verified.
+- Use approved reporting methods to report suspicious emails, texts, calls, or pop-ups.`,
+ },
+ {
+ title: '4. Quick reference: do / do not',
+ content: `| Do | Do not |
+|---|---|
+| Access only the PHI needed for your role. | Browse records out of curiosity or convenience. |
+| Confirm identity before sharing patient or employee information. | Discuss PHI in public areas, elevators, hallways, or on speakerphone where others can hear. |
+| Use approved encrypted channels and approved repositories. | Store PHI in personal email, personal drives, USB devices, or unapproved apps. |
+| Check recipients carefully before sending messages or attachments. | Rely on autofill without verifying the recipient. |
+| Report incidents, phishing, lost devices, or mistakes immediately. | Delay reporting because you hope the issue will resolve on its own. |`,
+ },
+ {
+ title: '5. Incident reporting expectation',
+ content: `Report immediately if you suspect a phishing email, accidental disclosure, misdirected message, unauthorized access, malware infection, stolen or missing device, or any situation that could affect the confidentiality, integrity, or availability of PHI.
+
+Prompt reporting matters even when you are unsure whether PHI was actually exposed. Early notice helps the organization contain risk and meet regulatory response obligations.`,
+ },
+] as const;
+
+export const hipaaAcknowledgements: readonly string[] = [
+ 'I completed the organization\'s general security awareness training and this HIPAA Security Awareness Training Add-On.',
+ 'I understand that PHI includes information in electronic, paper, image, audio, and verbal form.',
+ 'I will access, use, disclose, transmit, and store PHI only as authorized for my job responsibilities and in accordance with organization policy.',
+ 'I will use approved safeguards, including strong authentication, secure handling practices, and prompt reporting of suspicious activity or incidents.',
+ 'I understand that failure to follow security and privacy requirements may lead to disciplinary action, up to and including termination, and may create legal or regulatory consequences.',
+] as const;
diff --git a/bun.lock b/bun.lock
index f409289013..6ad098a13a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -148,7 +148,7 @@
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
- "globals": "^16.0.0",
+ "globals": "^17.3.0",
"jest": "^30.0.0",
"prettier": "^3.5.3",
"source-map-support": "^0.5.21",
@@ -657,7 +657,7 @@
"@tiptap/extension-highlight": "3.16.0",
"@tiptap/extension-image": "3.16.0",
"@tiptap/extension-link": "3.16.0",
- "@tiptap/extension-list": "3.16.0",
+ "@tiptap/extension-list": "3.18.0",
"@tiptap/extension-table": "3.16.0",
"@tiptap/extension-text-align": "3.16.0",
"@tiptap/extension-typography": "3.16.0",
@@ -2377,7 +2377,7 @@
"@tiptap/extension-link": ["@tiptap/extension-link@3.16.0", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.16.0", "@tiptap/pm": "^3.16.0" } }, "sha512-WPPJLtGXQadBVVwH6gcMpaXIgfvFF9NGpE2IVqleVKR3Epv2Rd4aWd4oyAdrT8KU9G6dzMXZfkrB8aArTDKxYQ=="],
- "@tiptap/extension-list": ["@tiptap/extension-list@3.16.0", "", { "peerDependencies": { "@tiptap/core": "^3.16.0", "@tiptap/pm": "^3.16.0" } }, "sha512-tpjWGugfI0XYR9iG/QlYYtCY35TFWHNwGKc94wN4s7NmAjB4xlwdTkTZQ6PdZ39x1SeHkRjxAka+6GcBIoOHGQ=="],
+ "@tiptap/extension-list": ["@tiptap/extension-list@3.18.0", "", { "peerDependencies": { "@tiptap/core": "^3.18.0", "@tiptap/pm": "^3.18.0" } }, "sha512-9lQBo45HNqIFcLEHAk+CY3W51eMMxIJjWbthm2CwEWr4PB3+922YELlvq8JcLH1nVFkBVpmBFmQe/GxgnCkzwQ=="],
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.16.0", "", { "peerDependencies": { "@tiptap/extension-list": "^3.16.0" } }, "sha512-kshssUZEPoosPWbJNQEFJnVV3iPwsDU9l/RCdHJB5SE+aNWJyUk5hQ/YwngEHjV7rS+RnAuhbrcB5swgyzROuA=="],
@@ -4079,7 +4079,7 @@
"global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="],
- "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+ "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
@@ -6779,8 +6779,6 @@
"@prisma/streams-local/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
- "@prisma/streams-local/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
-
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-checkbox/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
@@ -6899,6 +6897,8 @@
"@thallesp/nestjs-better-auth/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+ "@tiptap/starter-kit/@tiptap/extension-list": ["@tiptap/extension-list@3.16.0", "", { "peerDependencies": { "@tiptap/core": "^3.16.0", "@tiptap/pm": "^3.16.0" } }, "sha512-tpjWGugfI0XYR9iG/QlYYtCY35TFWHNwGKc94wN4s7NmAjB4xlwdTkTZQ6PdZ39x1SeHkRjxAka+6GcBIoOHGQ=="],
+
"@trigger.dev/core/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
"@trigger.dev/core/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json
index ac49c7243a..085d6f6fb3 100644
--- a/packages/docs/openapi.json
+++ b/packages/docs/openapi.json
@@ -18543,6 +18543,40 @@
]
}
},
+ "/v1/training/generate-hipaa-certificate": {
+ "post": {
+ "description": "Generates a PDF certificate for a member who has completed the HIPAA Security Awareness Training.",
+ "operationId": "TrainingController_generateHipaaCertificate_v1",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SendTrainingCompletionDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "PDF certificate file"
+ },
+ "400": {
+ "description": "HIPAA training not complete or member not found"
+ }
+ },
+ "security": [
+ {
+ "apikey": []
+ }
+ ],
+ "summary": "Generate HIPAA training certificate PDF",
+ "tags": [
+ "Training"
+ ]
+ }
+ },
"/v1/org-chart": {
"get": {
"operationId": "OrgChartController_getOrgChart_v1",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 412955e865..596c12b91c 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -43,7 +43,7 @@
"@tiptap/extension-highlight": "3.16.0",
"@tiptap/extension-image": "3.16.0",
"@tiptap/extension-link": "3.16.0",
- "@tiptap/extension-list": "3.16.0",
+ "@tiptap/extension-list": "3.18.0",
"@tiptap/extension-table": "3.16.0",
"@tiptap/extension-text-align": "3.16.0",
"@tiptap/extension-typography": "3.16.0",