diff --git a/apps/admin/locale/en.json b/apps/admin/locale/en.json
index 4a7e9f8c7..4e8d225ee 100644
--- a/apps/admin/locale/en.json
+++ b/apps/admin/locale/en.json
@@ -896,6 +896,34 @@
"sfid-label": "Salesforce Event ID",
"sfid-placeholder": "Enter your Salesforce Event ID",
"sfid-help": "The unique identifier of your event in Salesforce"
+ },
+ "sendgrid": {
+ "template-id-label": "SendGrid Template ID",
+ "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx",
+ "template-id-help": "Dynamic template ID from SendGrid",
+ "from-address-label": "From Email Address",
+ "from-address-placeholder": "noreply@example.com",
+ "from-address-help": "Verified sender email address",
+ "test-email-address-label": "Test Email Address",
+ "test-email-address-placeholder": "admin@example.com",
+ "test-email-address-help": "Where test emails will be sent",
+ "send-test-email-button": "Send Test Email",
+ "email-contacts-title": "Email Contacts",
+ "csv-description": "Upload a CSV file with columns: Team Number, Region, Recipient Name, Email Address",
+ "csv-requirements-title": "CSV File Requirements",
+ "csv-requirement-header": "File must include a header row",
+ "csv-requirement-columns": "Columns: Team Number, Region, Recipient Name, Recipient Email",
+ "csv-requirement-encoding": "Saved as UTF-8 encoded CSV",
+ "csv-example": "Example Format",
+ "upload-csv-button": "Upload CSV",
+ "validation-template-id-required": "Template ID is required",
+ "validation-from-address-required": "Valid email required",
+ "validation-test-email-required": "Valid email required",
+ "csv-error-invalid-format": "Please upload a CSV file",
+ "csv-error-upload-failed": "Failed to upload contacts",
+ "csv-error-send-test-failed": "Failed to send test email",
+ "csv-success-contacts-uploaded": "Successfully uploaded {count} email contacts",
+ "csv-success-test-email-sent": "Test email sent successfully to {email}"
}
}
},
@@ -903,6 +931,10 @@
"first-israel-dashboard": {
"name": "FIRST Israel Dashboard",
"description": "Upload team information and export results from your FIRST Israel Dashboard account"
+ },
+ "sendgrid": {
+ "name": "SendGrid",
+ "description": "Send event-related emails to teams using SendGrid email templates"
}
}
},
diff --git a/apps/admin/locale/he.json b/apps/admin/locale/he.json
index 6ed8e8ec0..974c79aae 100644
--- a/apps/admin/locale/he.json
+++ b/apps/admin/locale/he.json
@@ -896,6 +896,34 @@
"sfid-label": "מזהה אירוע של Salesforce",
"sfid-placeholder": "הזן את מזהה ה-Salesforce Event ID שלך",
"sfid-help": "המזהה הייחודי של האירוע שלך ב-Salesforce"
+ },
+ "sendgrid": {
+ "template-id-label": "SendGrid Template ID",
+ "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx",
+ "template-id-help": "מזהה תבנית דינמית מ-SendGrid",
+ "from-address-label": "כתובת דוא״ל מ",
+ "from-address-placeholder": "noreply@example.com",
+ "from-address-help": "כתובת דוא״ל של משלח מאומת",
+ "test-email-address-label": "כתובת דוא״ל לבדיקה",
+ "test-email-address-placeholder": "admin@example.com",
+ "test-email-address-help": "לאן ישלחו דוא״ל בדיקה",
+ "send-test-email-button": "שלח דוא״ל בדיקה",
+ "email-contacts-title": "אנשי קשר בדוא״ל",
+ "csv-description": "העלה קובץ CSV עם עמודות: Team Number, Region, Recipient Name, Email Address",
+ "csv-requirements-title": "דרישות קובץ CSV",
+ "csv-requirement-header": "הקובץ חייב לכלול שורת כותרת",
+ "csv-requirement-columns": "עמודות: Team Number, Region, Recipient Name, Recipient Email",
+ "csv-requirement-encoding": "שמור כקובץ CSV בקידוד UTF-8",
+ "csv-example": "פורמט דוגמה",
+ "upload-csv-button": "העלה CSV",
+ "validation-template-id-required": "מזהה תבנית נדרש",
+ "validation-from-address-required": "דוא״ל תקין נדרש",
+ "validation-test-email-required": "דוא״ל תקין נדרש",
+ "csv-error-invalid-format": "אנא העלה קובץ CSV",
+ "csv-error-upload-failed": "העלאת אנשי קשר נכשלה",
+ "csv-error-send-test-failed": "שליחת דוא״ל בדיקה נכשלה",
+ "csv-success-contacts-uploaded": "העלאת {count} אנשי קשר בדוא״ל בהצלחה",
+ "csv-success-test-email-sent": "דוא״ל בדיקה נשלח בהצלחה אל {email}"
}
}
},
@@ -903,6 +931,10 @@
"first-israel-dashboard": {
"name": "לוח בקרה FIRST Israel",
"description": "העלה מידע קבוצות וייצא תוצאות מחשבון לוח הבקרה של FIRST Israel שלך"
+ },
+ "sendgrid": {
+ "name": "SendGrid",
+ "description": "שלח דוא״ל הקשור לאירועים לקבוצות באמצעות תבניות דוא״ל של SendGrid"
}
}
},
diff --git a/apps/admin/locale/pl.json b/apps/admin/locale/pl.json
index e12e904a5..0625f9e11 100644
--- a/apps/admin/locale/pl.json
+++ b/apps/admin/locale/pl.json
@@ -896,6 +896,34 @@
"sfid-label": "ID wydarzenia w Salesforce",
"sfid-placeholder": "Wprowadź ID wydarzenia w Salesforce",
"sfid-help": "Unikalny identyfikator Twojego wydarzenia w Salesforce"
+ },
+ "sendgrid": {
+ "template-id-label": "SendGrid Template ID",
+ "template-id-placeholder": "d-xxxxxxxxxxxxxxxxxxxxxxxx",
+ "template-id-help": "Dynamiczny identyfikator szablonu z SendGrid",
+ "from-address-label": "Adres e-mail nadawcy",
+ "from-address-placeholder": "noreply@example.com",
+ "from-address-help": "Zweryfikowany adres e-mail nadawcy",
+ "test-email-address-label": "Testowy adres e-mail",
+ "test-email-address-placeholder": "admin@example.com",
+ "test-email-address-help": "Gdzie będą wysyłane testowe wiadomości e-mail",
+ "send-test-email-button": "Wyślij testową wiadomość e-mail",
+ "email-contacts-title": "Kontakty e-mail",
+ "csv-description": "Prześlij plik CSV z kolumnami: Team Number, Region, Recipient Name, Email Address",
+ "csv-requirements-title": "Wymagania dotyczące pliku CSV",
+ "csv-requirement-header": "Plik musi zawierać wiersz nagłówka",
+ "csv-requirement-columns": "Kolumny: Team Number, Region, Recipient Name, Recipient Email",
+ "csv-requirement-encoding": "Zapisane jako plik CSV w kodowaniu UTF-8",
+ "csv-example": "Format przykładowy",
+ "upload-csv-button": "Prześlij CSV",
+ "validation-template-id-required": "Wymagany identyfikator szablonu",
+ "validation-from-address-required": "Wymagany prawidłowy e-mail",
+ "validation-test-email-required": "Wymagany prawidłowy e-mail",
+ "csv-error-invalid-format": "Proszę przesłać plik CSV",
+ "csv-error-upload-failed": "Nie udało się przesłać kontaktów",
+ "csv-error-send-test-failed": "Nie udało się wysłać testową wiadomość e-mail",
+ "csv-success-contacts-uploaded": "Pomyślnie przesłano {count} kontaktów e-mail",
+ "csv-success-test-email-sent": "Testowa wiadomość e-mail została wysłana pomyślnie na adres {email}"
}
}
},
@@ -903,6 +931,10 @@
"first-israel-dashboard": {
"name": "FIRST Israel Dashboard",
"description": "Prześlij informacje o drużynach i eksportuj wyniki ze swojego konta FIRST Israel Dashboard"
+ },
+ "sendgrid": {
+ "name": "SendGrid",
+ "description": "Wyślij e-maile związane z wydarzeniem do drużyn za pomocą szablonów e-mail SendGrid"
}
}
},
diff --git a/apps/admin/public/assets/integration-icons/sendgrid.svg b/apps/admin/public/assets/integration-icons/sendgrid.svg
new file mode 100644
index 000000000..29a5c8ea0
--- /dev/null
+++ b/apps/admin/public/assets/integration-icons/sendgrid.svg
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx
new file mode 100644
index 000000000..cb86df829
--- /dev/null
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid-settings.tsx
@@ -0,0 +1,307 @@
+'use client';
+
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { useTranslations } from 'next-intl';
+import CloudUploadIcon from '@mui/icons-material/CloudUpload';
+import { Info as InfoIcon, Description as DescriptionIcon } from '@mui/icons-material';
+import {
+ Stack,
+ TextField,
+ Button,
+ Alert,
+ CircularProgress,
+ Box,
+ Typography,
+ Divider,
+ Paper,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText
+} from '@mui/material';
+import { SendGridSettingsSchema } from '@lems/shared/integrations';
+import { IntegrationSettingsComponentProps } from './settings-factory';
+
+interface SendGridFormValues {
+ templateId: string;
+ fromAddress: string;
+ testEmailAddress: string;
+}
+
+export const SendGridSettings: React.FC = ({
+ settings,
+ onSave,
+ isLoading = false,
+ showErrors = false
+}) => {
+ const t = useTranslations('pages.events.integrations.detail-panel.settings.sendgrid');
+
+ const [formValues, setFormValues] = useState({
+ templateId: '',
+ fromAddress: '',
+ testEmailAddress: ''
+ });
+ const [errors, setErrors] = useState>({});
+ const [csvError, setCsvError] = useState('');
+ const [csvSuccess, setCsvSuccess] = useState('');
+ const [isTestingEmail, setIsTestingEmail] = useState(false);
+ const fileInputRef = useRef(null);
+ const prevSettingsRef = useRef(null);
+ const hasInitializedRef = useRef(false);
+
+ // Initialize form state from settings
+ useEffect(() => {
+ const settingsStr = JSON.stringify(settings);
+ if (!hasInitializedRef.current || prevSettingsRef.current !== settingsStr) {
+ setFormValues({
+ templateId: (settings.templateId as string) || '',
+ fromAddress: (settings.fromAddress as string) || '',
+ testEmailAddress: (settings.testEmailAddress as string) || ''
+ });
+ setErrors({});
+ prevSettingsRef.current = settingsStr;
+ hasInitializedRef.current = true;
+ }
+ }, [settings]);
+
+ // Validate and save when showErrors is true
+ useEffect(() => {
+ if (showErrors) {
+ try {
+ const validated = SendGridSettingsSchema.parse(formValues);
+ setErrors({});
+ onSave(validated);
+ } catch (error) {
+ if (error instanceof Error) {
+ const message = error.message;
+ // Parse Zod error message to set individual field errors
+ if (message.includes('templateId'))
+ setErrors(e => ({ ...e, templateId: t('validation-template-id-required') }));
+ if (message.includes('fromAddress'))
+ setErrors(e => ({ ...e, fromAddress: t('validation-from-address-required') }));
+ if (message.includes('testEmailAddress'))
+ setErrors(e => ({ ...e, testEmailAddress: t('validation-test-email-required') }));
+ }
+ }
+ }
+ }, [showErrors, formValues, onSave, t]);
+
+ const handleFieldChange = useCallback(
+ (field: keyof SendGridFormValues, value: string) => {
+ setFormValues(prev => ({ ...prev, [field]: value }));
+ if (errors[field]) setErrors(prev => ({ ...prev, [field]: undefined }));
+ },
+ [errors]
+ );
+
+ const handleCSVUpload = async (file: File) => {
+ setCsvError('');
+ setCsvSuccess('');
+
+ if (!file.name.endsWith('.csv')) {
+ setCsvError(t('csv-error-invalid-format'));
+ return;
+ }
+
+ try {
+ const csvContent = await file.text();
+ const eventId = window.location.pathname.split('/')[3];
+
+ const response = await fetch(`/api/integrations/sendgrid/${eventId}/upload-contacts`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ csvContent })
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ setCsvError(error.error || t('csv-error-upload-failed'));
+ return;
+ }
+
+ const result = await response.json();
+ setCsvSuccess(t('csv-success-contacts-uploaded', { count: result.count }));
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ } catch (error) {
+ setCsvError(error instanceof Error ? error.message : t('csv-error-upload-failed'));
+ }
+ };
+
+ const handleTestEmail = async () => {
+ setIsTestingEmail(true);
+ try {
+ const eventId = window.location.pathname.split('/')[3];
+ const response = await fetch(`/api/integrations/sendgrid/${eventId}/send-test`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formValues)
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ setCsvError(error.error || t('csv-error-send-test-failed'));
+ return;
+ }
+
+ setCsvSuccess(t('csv-success-test-email-sent', { email: formValues.testEmailAddress }));
+ } catch (error) {
+ setCsvError(error instanceof Error ? error.message : t('csv-error-send-test-failed'));
+ } finally {
+ setIsTestingEmail(false);
+ }
+ };
+
+ return (
+
+
+ handleFieldChange('templateId', e.target.value)}
+ disabled={isLoading}
+ error={showErrors && !!errors.templateId}
+ helperText={showErrors && errors.templateId ? errors.templateId : t('template-id-help')}
+ size="small"
+ />
+
+
+
+ handleFieldChange('fromAddress', e.target.value)}
+ disabled={isLoading}
+ error={showErrors && !!errors.fromAddress}
+ helperText={
+ showErrors && errors.fromAddress ? errors.fromAddress : t('from-address-help')
+ }
+ size="small"
+ />
+
+
+
+ handleFieldChange('testEmailAddress', e.target.value)}
+ disabled={isLoading}
+ error={showErrors && !!errors.testEmailAddress}
+ helperText={
+ showErrors && errors.testEmailAddress
+ ? errors.testEmailAddress
+ : t('test-email-address-help')
+ }
+ size="small"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('email-contacts-title')}
+
+
+ {t('csv-description')}
+
+
+
+
+
+
+
+ {t('csv-requirements-title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('csv-example')}
+
+
+
+ {`Team Number,Region,Recipient Name,Recipient Email
+1234,North,John Doe,john@example.com
+5678,South,Jane Smith,jane@example.com
+9999,Central,Bob Johnson,bob@example.com`}
+
+
+
+
+ }
+ onClick={() => fileInputRef.current?.click()}
+ disabled={isLoading}
+ fullWidth
+ >
+ {t('upload-csv-button')}
+
+
+ {
+ const file = e.target.files?.[0];
+ if (file) handleCSVUpload(file);
+ }}
+ />
+
+
+ {csvError && {csvError}}
+ {csvSuccess && {csvSuccess}}
+
+ );
+};
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx
index dd5fdbb94..5785d3166 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/settings-factory.tsx
@@ -2,6 +2,7 @@
import { IntegrationType } from '@lems/shared/integrations';
import { FirstIsraelDashboardSettings } from './first-israel-dashboard-settings';
+import { SendGridSettings } from './sendgrid-settings';
/**
* Props passed to integration settings components
@@ -26,7 +27,8 @@ export type IntegrationSettingsComponent = React.FC = {
- 'first-israel-dashboard': FirstIsraelDashboardSettings
+ 'first-israel-dashboard': FirstIsraelDashboardSettings,
+ 'sendgrid': SendGridSettings
};
/**
diff --git a/apps/backend/.template.env b/apps/backend/.template.env
index 46f9ab1cb..16faf8434 100644
--- a/apps/backend/.template.env
+++ b/apps/backend/.template.env
@@ -35,4 +35,7 @@ SCHEDULER_JWT_SECRET=""
DIGITALOCEAN_ENDPOINT=""
DIGITALOCEAN_SPACE=""
DIGITALOCEAN_KEY=""
-DIGITALOCEAN_SECRET=""
\ No newline at end of file
+DIGITALOCEAN_SECRET=""
+
+# SendGrid Email Integration
+SENDGRID_API_KEY=""
\ No newline at end of file
diff --git a/apps/backend/src/routers/admin/events/settings/index.ts b/apps/backend/src/routers/admin/events/settings/index.ts
index 5426b0320..04a8c3492 100644
--- a/apps/backend/src/routers/admin/events/settings/index.ts
+++ b/apps/backend/src/routers/admin/events/settings/index.ts
@@ -1,6 +1,8 @@
import express from 'express';
+import { IntegrationTypes } from '@lems/shared/integrations';
import db from '../../../../lib/database';
import { AdminEventRequest } from '../../../../types/express';
+import { publishEventResults } from '../../../integrations/sendgrid/publish';
import { makeAdminSettingsResponse, makeUpdateableEventSettings } from './util';
const router = express.Router({ mergeParams: true });
@@ -28,16 +30,41 @@ router.post('/complete', async (req: AdminEventRequest, res) => {
});
router.post('/publish', async (req: AdminEventRequest, res) => {
- const settings = await db.events.byId(req.eventId).getSettings();
- if (!settings.completed) {
- res.json()
- }
+ try {
+ const settings = await db.events.byId(req.eventId).getSettings();
+ if (!settings.completed) {
+ res.status(400).json({ error: 'Event must be completed before publishing' });
+ return;
+ }
- const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true });
- if (!updatedSettings) {
- throw new Error('Failed to publish event');
+ const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true });
+ if (!updatedSettings) {
+ throw new Error('Failed to publish event');
+ }
+
+ // Send emails if SendGrid integration is enabled
+ const integrations = await db.integrations.byEventId(req.eventId).getAll();
+ const sendgridIntegration = integrations.find(
+ i => i.integration_type === IntegrationTypes.SENDGRID && i.enabled
+ );
+
+ if (sendgridIntegration) {
+ try {
+ await publishEventResults({
+ eventId: req.eventId,
+ settings: sendgridIntegration.settings
+ });
+ } catch (error) {
+ console.error('Error sending SendGrid emails:', error);
+ // Don't fail the publish - log the error but continue
+ }
+ }
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Error publishing event:', error);
+ res.status(500).json({ error: 'Failed to publish event' });
}
- res.json({ success: true });
});
export default router;
diff --git a/apps/backend/src/routers/integrations/index.ts b/apps/backend/src/routers/integrations/index.ts
index 1577b07b0..9484ed650 100644
--- a/apps/backend/src/routers/integrations/index.ts
+++ b/apps/backend/src/routers/integrations/index.ts
@@ -1,8 +1,10 @@
import express from 'express';
import firstIsraelDashboardRouter from './first-israel-dashboard';
+import sendgridRouter from './sendgrid';
const router = express.Router({ mergeParams: true });
router.use('/first-israel-dashboard', firstIsraelDashboardRouter);
+router.use('/sendgrid', sendgridRouter);
export default router;
diff --git a/apps/backend/src/routers/integrations/sendgrid/index.ts b/apps/backend/src/routers/integrations/sendgrid/index.ts
new file mode 100644
index 000000000..ed7ef86cc
--- /dev/null
+++ b/apps/backend/src/routers/integrations/sendgrid/index.ts
@@ -0,0 +1,154 @@
+import express from 'express';
+import { parse } from 'csv-parse/sync';
+import { AdminEventRequest } from '../../../types/express';
+import { requirePermission } from '../../../routers/admin/middleware/require-permission';
+import db from '../../../lib/database';
+import { sendEmailWithSendGrid } from './sendgrid-lib';
+import { generatePlaceholderPDF } from './placeholder-generator';
+import { publishEventResults } from './publish';
+import { CSVRecord } from './types';
+
+const router = express.Router({ mergeParams: true });
+
+interface PublishRequest extends AdminEventRequest {
+ body: {
+ settings: Record;
+ };
+}
+
+router.post(
+ '/:eventId/upload-contacts',
+ requirePermission('MANAGE_EVENT_DETAILS'),
+ async (req: AdminEventRequest, res) => {
+ try {
+ const { csvContent } = req.body;
+ if (!csvContent) {
+ res.status(400).json({ error: 'No CSV content provided' });
+ return;
+ }
+
+ const csvText =
+ typeof csvContent === 'string' ? csvContent : Buffer.from(csvContent).toString('utf-8');
+
+ const records = parse(csvText, {
+ columns: ['team_number', 'region', 'recipient_name', 'recipient_email'],
+ skip_empty_lines: true,
+ from_line: 2 // Skip header row
+ }) as CSVRecord[];
+
+ if (!Array.isArray(records) || records.length === 0) {
+ res.status(400).json({ error: 'CSV file is empty or invalid' });
+ return;
+ }
+
+ // Validate email format
+ const validRecords = records.filter((record: CSVRecord) => {
+ const email = record.recipient_email?.toString().trim();
+ return email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
+ });
+
+ if (validRecords.length === 0) {
+ res.status(400).json({ error: 'No valid email addresses found in CSV' });
+ return;
+ }
+
+ // Get current integration and update with base64-encoded CSV data
+ const eventId = req.params.eventId;
+ const integration = await db.integrations.byType(eventId, 'sendgrid').get();
+ if (!integration) {
+ res.status(404).json({ error: 'SendGrid integration not found' });
+ return;
+ }
+
+ // Encode CSV content to base64
+ const emailContactsData = Buffer.from(csvContent).toString('base64');
+
+ // Update integration settings with encoded data
+ const updatedSettings = {
+ ...integration.settings,
+ emailContactsData
+ };
+
+ await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings });
+
+ res.json({ count: validRecords.length });
+ } catch (error) {
+ console.error('Error uploading contacts:', error);
+ res.status(500).json({ error: 'Failed to process CSV file' });
+ }
+ }
+);
+
+router.post(
+ '/:eventId/send-test',
+ requirePermission('MANAGE_EVENT_DETAILS'),
+ async (req: AdminEventRequest, res) => {
+ try {
+ const { templateId, fromAddress, testEmailAddress } = req.body;
+
+ if (!templateId || !fromAddress || !testEmailAddress) {
+ res.status(400).json({ error: 'Missing required settings' });
+ return;
+ }
+
+ const apiKey = process.env.SENDGRID_API_KEY;
+ if (!apiKey) {
+ res.status(500).json({ error: 'SendGrid API key not configured' });
+ return;
+ }
+
+ // Generate placeholder PDF
+ const pdfBuffer = await generatePlaceholderPDF();
+ const pdfBase64 = pdfBuffer.toString('base64');
+
+ await sendEmailWithSendGrid({
+ apiKey,
+ from: fromAddress,
+ to: testEmailAddress,
+ templateId,
+ dynamicTemplateData: {
+ eventName: 'Test Event',
+ teamNumber: 0,
+ recipientName: 'Test User'
+ },
+ attachments: [
+ {
+ filename: 'scoresheet.pdf',
+ content: pdfBase64,
+ type: 'application/pdf'
+ },
+ {
+ filename: 'rubric.pdf',
+ content: pdfBase64,
+ type: 'application/pdf'
+ }
+ ]
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Error sending test email:', error);
+ res
+ .status(500)
+ .json({ error: error instanceof Error ? error.message : 'Failed to send test email' });
+ }
+ }
+);
+
+// Publish event results via email
+router.post('/:eventId/publish', async (req: PublishRequest, res) => {
+ try {
+ const result = await publishEventResults({
+ eventId: req.params.eventId,
+ settings: req.body.settings
+ });
+ res.json(result);
+ } catch (error) {
+ console.error('Error publishing event results:', error);
+ res.status(500).json({
+ error: error instanceof Error ? error.message : 'Failed to publish event results'
+ });
+ }
+});
+
+export default router;
diff --git a/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts b/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts
new file mode 100644
index 000000000..f6e0b7501
--- /dev/null
+++ b/apps/backend/src/routers/integrations/sendgrid/placeholder-generator.ts
@@ -0,0 +1,52 @@
+import puppeteer from 'puppeteer';
+
+/**
+ * TODO: Replace this placeholder with actual scoresheet/rubric PDF generation
+ * This should query the database for event results and generate PDFs from templates
+ */
+export async function generatePlaceholderPDF(): Promise {
+ let browser;
+ try {
+ browser = await puppeteer.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
+ });
+
+ const page = await browser.newPage();
+
+ // Generate empty A4 page with basic styling
+ await page.setContent(`
+
+
+
+
+
+
+
+
+
+ `);
+
+ const pdf = await page.pdf({
+ format: 'A4',
+ margin: { top: 0, right: 0, bottom: 0, left: 0 }
+ });
+
+ return pdf || Buffer.alloc(0);
+ } finally {
+ if (browser) {
+ await browser.close();
+ }
+ }
+}
diff --git a/apps/backend/src/routers/integrations/sendgrid/publish.ts b/apps/backend/src/routers/integrations/sendgrid/publish.ts
new file mode 100644
index 000000000..7e317ec35
--- /dev/null
+++ b/apps/backend/src/routers/integrations/sendgrid/publish.ts
@@ -0,0 +1,95 @@
+import { parse } from 'csv-parse/sync';
+import db from '../../../lib/database';
+import { sendEmailWithSendGrid } from './sendgrid-lib';
+import { generatePlaceholderPDF } from './placeholder-generator';
+import { CSVRecord } from './types';
+
+export interface SendGridPublishOptions {
+ eventId: string;
+ settings: Record;
+}
+
+export async function publishEventResults(options: SendGridPublishOptions) {
+ const { eventId, settings } = options;
+
+ if (!settings) {
+ throw new Error('Missing integration settings');
+ }
+
+ const apiKey = process.env.SENDGRID_API_KEY;
+ if (!apiKey) {
+ throw new Error('SendGrid API key not configured');
+ }
+
+ const { templateId, fromAddress, emailContactsData } = settings;
+ if (!templateId || !fromAddress) {
+ throw new Error('SendGrid integration missing required settings');
+ }
+
+ if (!emailContactsData) {
+ throw new Error('No email contacts configured for event');
+ }
+
+ // Decode base64 CSV data
+ const csvContent = Buffer.from(emailContactsData as string, 'base64').toString('utf-8');
+ const contacts = parse(csvContent, {
+ columns: ['team_number', 'region', 'recipient_name', 'recipient_email'],
+ skip_empty_lines: true,
+ from_line: 2 // Skip header row
+ });
+
+ if (!Array.isArray(contacts) || contacts.length === 0) {
+ throw new Error('No email contacts in CSV data');
+ }
+
+ const event = await db.events.byId(eventId).get();
+ const pdfBuffer = await generatePlaceholderPDF();
+ const pdfBase64 = pdfBuffer.toString('base64');
+
+ const failedEmails: string[] = [];
+
+ // Send emails to each contact
+ for (const contact of contacts) {
+ try {
+ const typedContact = contact as CSVRecord;
+ const teamNumber = parseInt(typedContact.team_number, 10);
+ await sendEmailWithSendGrid({
+ apiKey,
+ from: fromAddress as string,
+ to: typedContact.recipient_email?.toString().trim() || '',
+ toName: typedContact.recipient_name?.toString().trim(),
+ templateId: templateId as string,
+ dynamicTemplateData: {
+ eventName: event?.name || 'Event',
+ teamNumber,
+ recipientName: typedContact.recipient_name,
+ region: typedContact.region
+ },
+ attachments: [
+ {
+ filename: `team-${teamNumber}-scoresheet.pdf`,
+ content: pdfBase64,
+ type: 'application/pdf'
+ },
+ {
+ filename: `team-${teamNumber}-rubric.pdf`,
+ content: pdfBase64,
+ type: 'application/pdf'
+ }
+ ]
+ });
+ } catch (error) {
+ const typedContact = contact as CSVRecord;
+ const email = typedContact.recipient_email;
+ failedEmails.push(email);
+ console.error(`Failed to send email to ${email}:`, error);
+ }
+ }
+
+ return {
+ success: true,
+ total: contacts.length,
+ failed: failedEmails.length,
+ failedEmails: failedEmails.length > 0 ? failedEmails : undefined
+ };
+}
diff --git a/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts b/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts
new file mode 100644
index 000000000..f6bf66638
--- /dev/null
+++ b/apps/backend/src/routers/integrations/sendgrid/sendgrid-lib.ts
@@ -0,0 +1,46 @@
+import sgMail from '@sendgrid/mail';
+
+interface EmailAttachment {
+ filename: string;
+ content: string; // base64 encoded
+ type: string;
+}
+
+interface SendEmailParams {
+ apiKey: string;
+ from: string;
+ to: string;
+ toName?: string;
+ templateId: string;
+ dynamicTemplateData: Record;
+ attachments?: EmailAttachment[];
+}
+
+export async function sendEmailWithSendGrid(params: SendEmailParams): Promise {
+ sgMail.setApiKey(params.apiKey);
+
+ const mail = {
+ to: {
+ email: params.to,
+ name: params.toName
+ },
+ from: params.from,
+ templateId: params.templateId,
+ dynamicTemplateData: params.dynamicTemplateData,
+ attachments: params.attachments?.map(att => ({
+ filename: att.filename,
+ content: att.content,
+ type: att.type,
+ disposition: 'attachment' as const
+ }))
+ };
+
+ try {
+ await sgMail.send(mail);
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Failed to send email to ${params.to}: ${error.message}`);
+ }
+ throw error;
+ }
+}
diff --git a/apps/backend/src/routers/integrations/sendgrid/types.ts b/apps/backend/src/routers/integrations/sendgrid/types.ts
new file mode 100644
index 000000000..4259aa340
--- /dev/null
+++ b/apps/backend/src/routers/integrations/sendgrid/types.ts
@@ -0,0 +1,6 @@
+export interface CSVRecord {
+ team_number: string;
+ region: string;
+ recipient_name: string;
+ recipient_email: string;
+}
diff --git a/compose.yml b/compose.yml
index a68adacdb..adf762110 100644
--- a/compose.yml
+++ b/compose.yml
@@ -42,6 +42,9 @@ services:
- DIGITALOCEAN_KEY=${DIGITALOCEAN_KEY}
- DIGITALOCEAN_SECRET=${DIGITALOCEAN_SECRET}
+ # Integrations
+ - SENDGRID_API_KEY=${SENDGRID_API_KEY}
+
admin:
image: ${REGISTRY}/lems:admin-${IMAGE_TAG}
ports:
diff --git a/libs/shared/src/lib/integrations.ts b/libs/shared/src/lib/integrations.ts
index ba9e2007d..73906d01d 100644
--- a/libs/shared/src/lib/integrations.ts
+++ b/libs/shared/src/lib/integrations.ts
@@ -1,7 +1,8 @@
import { z } from 'zod';
export const IntegrationTypes = {
- FIRST_ISRAEL_DASHBOARD: 'first-israel-dashboard'
+ FIRST_ISRAEL_DASHBOARD: 'first-israel-dashboard',
+ SENDGRID: 'sendgrid'
} as const;
export type IntegrationType = (typeof IntegrationTypes)[keyof typeof IntegrationTypes];
@@ -12,12 +13,33 @@ export const FirstIsraelDashboardSettingsSchema = z.object({
export type FirstIsraelDashboardSettings = z.infer;
-export const IntegrationSettingsSchema = z.union([FirstIsraelDashboardSettingsSchema]);
+export const SendGridSettingsSchema = z.object({
+ templateId: z.string().nullable().default(null).describe('SendGrid Dynamic Template ID'),
+ fromAddress: z
+ .email('Must be a valid email')
+ .nullable()
+ .default(null)
+ .describe('Sender email address'),
+ testEmailAddress: z
+ .email('Must be a valid email')
+ .nullable()
+ .default(null)
+ .describe('Test recipient email address'),
+ emailContactsData: z.string().optional().describe('Base64-encoded CSV contact data')
+});
+
+export type SendGridSettings = z.infer;
+
+export const IntegrationSettingsSchema = z.union([
+ FirstIsraelDashboardSettingsSchema,
+ SendGridSettingsSchema
+]);
export type IntegrationSettings = z.infer;
const INTEGRATION_LOGOS: Record = {
- [IntegrationTypes.FIRST_ISRAEL_DASHBOARD]: 'integration-icons/first-israel-dashboard.svg'
+ [IntegrationTypes.FIRST_ISRAEL_DASHBOARD]: 'integration-icons/first-israel-dashboard.svg',
+ [IntegrationTypes.SENDGRID]: 'integration-icons/sendgrid.svg'
} as const;
export interface IntegrationConfig {
@@ -31,6 +53,11 @@ const INTEGRATIONS_REGISTRY: Record = {
type: IntegrationTypes.FIRST_ISRAEL_DASHBOARD,
settingsSchema: FirstIsraelDashboardSettingsSchema,
logoAsset: INTEGRATION_LOGOS[IntegrationTypes.FIRST_ISRAEL_DASHBOARD]
+ },
+ [IntegrationTypes.SENDGRID]: {
+ type: IntegrationTypes.SENDGRID,
+ settingsSchema: SendGridSettingsSchema,
+ logoAsset: INTEGRATION_LOGOS[IntegrationTypes.SENDGRID]
}
};
diff --git a/package-lock.json b/package-lock.json
index 2c9c5c0e5..9cc1c5d83 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"@mui/stylis-plugin-rtl": "^7.3.6",
"@mui/x-data-grid": "^8.25.0",
"@mui/x-date-pickers": "^8.24.0",
+ "@sendgrid/mail": "^8.1.6",
"@uiw/react-color": "^2.9.2",
"@uiw/react-signature": "^1.3.3",
"axios": "^1.13.2",
@@ -149,6 +150,7 @@
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.0.tgz",
"integrity": "sha512-N/nZXGNBMoHnshNaHXxHZoC42BcIjRqD4XgpmNBPhueoWIbp17VIJe/sGysFNQo1w7DVD78K6gVsNMO87nfgRQ==",
"license": "MIT",
+ "peer": true,
"workspaces": [
"dist",
"codegen",
@@ -248,6 +250,7 @@
"resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.2.0.tgz",
"integrity": "sha512-OEAl5bwVitkvVkmZlgWksSnQ10FUr6q2qJMdkexs83lsvOGmd/y81X5LoETmKZux8UiQsy/A/xzP00b8hTHH/w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@apollo/cache-control-types": "^1.0.3",
"@apollo/server-gateway-interface": "^2.0.0",
@@ -1866,6 +1869,7 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -3755,6 +3759,7 @@
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -3920,6 +3925,7 @@
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
@@ -3960,6 +3966,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -3997,6 +4004,7 @@
"resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz",
"integrity": "sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@emotion/utils": "^1.2.1",
"html-tokenize": "^2.0.0",
@@ -4023,6 +4031,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -4897,6 +4906,7 @@
"resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.37.3.tgz",
"integrity": "sha512-rNJjwsYGhcZRdZ2FnyU6ss06xQaZ4UordyvOhp7+b/bEqQiEBpMOLJjuUr48Z6T7zEbZBnzCJpIJyXNqlcfQeA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@graphiql/toolkit": "^0.11.3",
"@radix-ui/react-dialog": "^1.1",
@@ -6663,6 +6673,7 @@
"integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@module-federation/runtime": "0.22.0",
"@module-federation/webpack-bundler-runtime": "0.22.0"
@@ -6806,6 +6817,7 @@
"integrity": "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@module-federation/runtime": "0.21.6",
"@module-federation/webpack-bundler-runtime": "0.21.6"
@@ -6969,6 +6981,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz",
"integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.6",
@@ -7134,6 +7147,7 @@
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz",
"integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.6",
@@ -11428,6 +11442,7 @@
"integrity": "sha512-uDxPQsPh/+2DnOISuKnUiXZ9M0y2G1BOsI0IesxPJGp42ME2QW7axbJfUqD3bwp4bi3RN2zqh56NgxU/XETQvA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@module-federation/runtime-tools": "0.22.0",
"@rspack/binding": "1.7.0",
@@ -11524,6 +11539,44 @@
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
+ "node_modules/@sendgrid/client": {
+ "version": "8.1.6",
+ "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.6.tgz",
+ "integrity": "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sendgrid/helpers": "^8.0.0",
+ "axios": "^1.12.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
+ "node_modules/@sendgrid/helpers": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
+ "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.2.2"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/@sendgrid/mail": {
+ "version": "8.1.6",
+ "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.6.tgz",
+ "integrity": "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sendgrid/client": "^8.1.5",
+ "@sendgrid/helpers": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
@@ -12483,6 +12536,7 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -12609,6 +12663,7 @@
"integrity": "sha512-VQ0hJ5jX31TVv/fhZx4xJRzd8pwn6VvzYd2tGOHHr2TfXGCBixZoqdPDXTiEoJLCTS2MmvBf6zyQZZ0M8aGQCQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@swc-node/core": "^1.14.1",
"@swc-node/sourcemap-support": "^0.6.1",
@@ -12740,6 +12795,7 @@
"integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==",
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
@@ -12962,6 +13018,7 @@
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@swc/counter": "^0.1.3"
}
@@ -13180,7 +13237,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
@@ -13295,8 +13351,7 @@
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/@types/eslint": {
"version": "9.6.1",
@@ -13515,6 +13570,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -13836,6 +13892,7 @@
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.52.0",
"@typescript-eslint/types": "8.52.0",
@@ -14808,7 +14865,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
@@ -14828,7 +14884,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@vitest/spy": "4.0.16",
"estree-walker": "^3.0.3",
@@ -14857,7 +14912,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -14869,7 +14923,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"tinyrainbow": "^3.0.3"
},
@@ -14884,7 +14937,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@vitest/utils": "4.0.16",
"pathe": "^2.0.3"
@@ -14900,7 +14952,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@vitest/pretty-format": "4.0.16",
"magic-string": "^0.30.21",
@@ -14917,7 +14968,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"funding": {
"url": "https://opencollective.com/vitest"
}
@@ -14929,7 +14979,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@vitest/pretty-format": "4.0.16",
"tinyrainbow": "^3.0.3"
@@ -15340,6 +15389,7 @@
"integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -15365,6 +15415,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -15441,6 +15492,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -15820,7 +15872,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -16168,6 +16219,7 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -16248,6 +16300,7 @@
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
+ "peer": true,
"peerDependencies": {
"bare-abort-controller": "*"
},
@@ -16609,6 +16662,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -16919,7 +16973,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -18099,7 +18152,8 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/csv-parse": {
"version": "6.1.0",
@@ -18303,6 +18357,7 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -18331,7 +18386,8 @@
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/debounce-promise": {
"version": "3.1.2",
@@ -18443,7 +18499,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -18682,7 +18737,8 @@
"version": "0.0.1534754",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
"integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
- "license": "BSD-3-Clause"
+ "license": "BSD-3-Clause",
+ "peer": true
},
"node_modules/diff": {
"version": "4.0.2",
@@ -19216,8 +19272,7 @@
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
@@ -19393,6 +19448,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -19493,6 +19549,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -19594,6 +19651,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -20191,7 +20249,6 @@
"dev": true,
"license": "Apache-2.0",
"optional": true,
- "peer": true,
"engines": {
"node": ">=12.0.0"
}
@@ -20201,6 +20258,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -20604,6 +20662,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -20971,6 +21030,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -21841,6 +21901,7 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -21897,6 +21958,7 @@
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz",
"integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20"
},
@@ -22543,6 +22605,7 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -24567,6 +24630,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
@@ -25008,6 +25072,7 @@
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
}
@@ -25958,7 +26023,8 @@
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/monaco-graphql": {
"version": "1.7.3",
@@ -26281,6 +26347,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
"integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@next/env": "16.1.1",
"@swc/helpers": "0.5.15",
@@ -26626,6 +26693,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@napi-rs/wasm-runtime": "0.2.4",
"@yarnpkg/lockfile": "^1.1.0",
@@ -26939,8 +27007,7 @@
"https://opencollective.com/debug"
],
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
@@ -27472,8 +27539,7 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/pend": {
"version": "1.2.0",
@@ -27492,6 +27558,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz",
"integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"pg-connection-string": "^2.10.0",
"pg-pool": "^3.11.0",
@@ -27888,6 +27955,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -28667,6 +28735,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -29072,6 +29141,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -29099,6 +29169,7 @@
"resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.1.tgz",
"integrity": "sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw==",
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental"
}
@@ -29108,6 +29179,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -29151,7 +29223,8 @@
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz",
"integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-markdown": {
"version": "10.1.0",
@@ -29417,7 +29490,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -29725,6 +29799,7 @@
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -30741,6 +30816,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -30862,6 +30938,7 @@
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -30883,6 +30960,7 @@
"integrity": "sha512-wH3CbOThHYGX0bUyqFf7laLKyhVWIFc2lHynitkqMIUCtX2ixH9mQh0bN7+hkUu5BFt/SXvEMjFbkEbBMpQiSQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
"buffer-builder": "^0.2.0",
@@ -31934,8 +32012,7 @@
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/signal-exit": {
"version": "3.0.7",
@@ -32002,6 +32079,7 @@
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
@@ -32283,8 +32361,7 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/standard-as-callback": {
"version": "2.1.0",
@@ -32307,8 +32384,7 @@
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
@@ -32802,7 +32878,8 @@
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/supports-color": {
"version": "7.2.0",
@@ -33009,6 +33086,7 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -33282,8 +33360,7 @@
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/tinycolor2": {
"version": "1.6.0",
@@ -33298,7 +33375,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -33340,7 +33416,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -33544,6 +33619,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -33627,7 +33703,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
+ "license": "0BSD",
+ "peer": true
},
"node_modules/tsscmp": {
"version": "1.0.6",
@@ -33645,6 +33722,7 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -33805,6 +33883,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -34342,595 +34421,6 @@
"d3-timer": "^3.0.1"
}
},
- "node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "esbuild": "^0.25.0",
- "fdir": "^6.4.4",
- "picomatch": "^4.0.2",
- "postcss": "^8.5.3",
- "rollup": "^4.34.9",
- "tinyglobby": "^0.2.13"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "jiti": ">=1.21.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vite/node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
"node_modules/vitest": {
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
@@ -34938,7 +34428,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16",
@@ -35018,7 +34507,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -35108,6 +34596,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -35264,6 +34753,7 @@
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -36024,7 +35514,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
@@ -36192,6 +35681,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -36271,6 +35761,7 @@
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"license": "ISC",
+ "peer": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -36370,6 +35861,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index b306543bc..394fbe174 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"@mui/stylis-plugin-rtl": "^7.3.6",
"@mui/x-data-grid": "^8.25.0",
"@mui/x-date-pickers": "^8.24.0",
+ "@sendgrid/mail": "^8.1.6",
"@uiw/react-color": "^2.9.2",
"@uiw/react-signature": "^1.3.3",
"axios": "^1.13.2",