mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
Add communication health panel
This commit is contained in:
parent
4577f67845
commit
322bc1f1f7
6 changed files with 397 additions and 6 deletions
|
|
@ -120,6 +120,10 @@ Use the Squad Chat Webhook only when local chat needs to notify an external syst
|
|||
| Notifications | Stores recipient-specific task/system records, including failure alerts | Optional delivery channel sends externally if configured |
|
||||
| Broadcasts | Stores durable system-wide messages at `/api/broadcasts` for polling/UI | Agents poll or receive WebSocket updates and mark messages read |
|
||||
|
||||
Settings -> Notifications -> Communication Health reports whether each path is configured and whether VK saw the last outbound HTTP result. HTTP success is not visual receipt. For Teams-style workflows, webhook receivers, and OpenClaw gateways, verify the destination manually after VK records a successful delivery.
|
||||
|
||||
Generic Squad Chat webhooks send a `squad.message` JSON payload with `event`, `message.id`, `message.agent`, `message.message`, `message.timestamp`, and `isHuman`. When a secret is set, VK signs the request with `X-VK-Signature`. OpenClaw Direct posts a wake payload to `/tools/invoke` with bearer auth. Secrets, bearer tokens, query strings, and webhook paths should stay out of logs, screenshots, and support notes.
|
||||
|
||||
## Step-by-Step: Tag Conventions
|
||||
|
||||
Use consistent tags so messages are filterable by project or task:
|
||||
|
|
|
|||
|
|
@ -538,12 +538,21 @@ openclaw mcp describe veritas-kanban vk_list_tasks
|
|||
|
||||
Squad Chat stores local messages and streams them to connected UI clients. It does not wake an external process by itself. Configure Settings -> Notifications -> Squad Chat Webhook only when you want outbound delivery through a generic webhook or OpenClaw Direct.
|
||||
|
||||
If the webhook request returns HTTP success but nothing appears externally, split the failure path:
|
||||
Use Settings -> Notifications -> Communication Health to separate three different states:
|
||||
|
||||
- Configured: VK has a destination URL, channel, or OpenClaw gateway setting.
|
||||
- HTTP accepted: the last outbound delivery received an HTTP response such as `2xx`.
|
||||
- Visually verified: a human confirmed the message appeared in the destination. VK does not record this automatically.
|
||||
|
||||
Smoke-test the path in this order:
|
||||
|
||||
1. Local chat: verify the message exists in the Squad Chat UI or `GET /api/chat/squad`
|
||||
2. Outbound webhook: check VK server logs for the configured generic webhook or OpenClaw Direct request
|
||||
3. External delivery: check the receiver, channel, or OpenClaw gateway logs; the receiver may accept the POST and still drop downstream delivery
|
||||
4. Agent runner: confirm the external orchestrator is configured to wake an agent and post any visible reply back to `/api/chat/squad`
|
||||
2. Generic webhook: configure a disposable receiver, post a human or agent Squad Chat message, and confirm VK records the outbound HTTP result
|
||||
3. Teams-style workflow: confirm the receiver or workflow returns HTTP success, then separately verify the message is visible in Teams
|
||||
4. OpenClaw Direct: confirm the gateway accepts `/tools/invoke`, then confirm the external orchestrator wakes an agent and posts any visible reply back to `/api/chat/squad`
|
||||
5. Failure alerts: configure Failure Webhook URL if you need immediate external delivery, trigger or use the configured test path, then verify both the HTTP result and the destination-visible alert
|
||||
|
||||
Generic Squad Chat webhooks send a `squad.message` payload with `event`, `message.id`, `message.agent`, `message.message`, `message.timestamp`, and `isHuman`. If a webhook secret is configured, VK signs the request with `X-VK-Signature`. The Communication Health panel redacts webhook paths, query strings, secrets, and bearer tokens; avoid pasting full webhook URLs into screenshots or support notes.
|
||||
|
||||
### Notifications do not send externally
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,65 @@ import { renderWithProviders } from './test-utils';
|
|||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
debouncedUpdate: vi.fn(),
|
||||
outboundEndpoints: vi.fn(async () => [
|
||||
{
|
||||
id: 'squad.webhook',
|
||||
type: 'squad-webhook',
|
||||
displayName: 'Squad Chat webhook',
|
||||
url: 'https://example.com/webhook',
|
||||
enabled: true,
|
||||
auth: {
|
||||
type: 'hmac-sha256',
|
||||
headerName: 'X-VK-Signature',
|
||||
secretRef: 'featureSettings.squadWebhook.secret',
|
||||
hasSecret: true,
|
||||
},
|
||||
validation: { valid: true },
|
||||
updatedAt: '2026-06-04T08:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'notifications.failureAlert',
|
||||
type: 'failure-alert-webhook',
|
||||
displayName: 'Failure alert webhook',
|
||||
url: 'https://example.com/failure-alerts',
|
||||
enabled: true,
|
||||
auth: { type: 'none' },
|
||||
validation: { valid: true },
|
||||
updatedAt: '2026-06-04T08:00:00.000Z',
|
||||
},
|
||||
]),
|
||||
outboundDeliveries: vi.fn(async () => [
|
||||
{
|
||||
id: 'delivery_1',
|
||||
endpointId: 'squad.webhook',
|
||||
endpointType: 'squad-webhook',
|
||||
displayName: 'Squad Chat webhook',
|
||||
method: 'POST',
|
||||
sanitizedUrl: 'https://example.com/webhook',
|
||||
status: 'success',
|
||||
responseStatus: 202,
|
||||
responseClass: '2xx',
|
||||
durationMs: 42,
|
||||
attempt: 1,
|
||||
startedAt: '2026-06-04T08:00:00.000Z',
|
||||
completedAt: '2026-06-04T08:00:01.000Z',
|
||||
},
|
||||
{
|
||||
id: 'delivery_2',
|
||||
endpointId: 'notifications.failureAlert',
|
||||
endpointType: 'failure-alert-webhook',
|
||||
displayName: 'Failure alert webhook',
|
||||
method: 'POST',
|
||||
sanitizedUrl: 'https://example.com/failure-alerts',
|
||||
status: 'failed',
|
||||
responseStatus: 500,
|
||||
responseClass: '5xx',
|
||||
durationMs: 57,
|
||||
attempt: 1,
|
||||
startedAt: '2026-06-04T08:00:00.000Z',
|
||||
completedAt: '2026-06-04T08:00:02.000Z',
|
||||
},
|
||||
]),
|
||||
settings: {
|
||||
board: {},
|
||||
tasks: {},
|
||||
|
|
@ -18,6 +77,7 @@ const mocks = vi.hoisted(() => ({
|
|||
onAgentFailure: true,
|
||||
onReviewNeeded: true,
|
||||
channel: '19:test@thread.tacv2',
|
||||
webhookUrl: 'https://example.com/failure-alerts?token=hidden',
|
||||
},
|
||||
squadWebhook: {
|
||||
enabled: true,
|
||||
|
|
@ -49,6 +109,15 @@ vi.mock('@/hooks/useFeatureSettings', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/api', () => ({
|
||||
api: {
|
||||
integrations: {
|
||||
outboundEndpoints: mocks.outboundEndpoints,
|
||||
outboundDeliveries: mocks.outboundDeliveries,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useConfig', () => ({
|
||||
useConfig: () => ({
|
||||
data: {
|
||||
|
|
@ -83,10 +152,17 @@ describe('Settings tab Mantine controls', () => {
|
|||
expect(tasksContainer.querySelector('.mantine-Select-root')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders Notifications text and select controls through direct Mantine primitives', () => {
|
||||
it('renders Notifications text and select controls through direct Mantine primitives', async () => {
|
||||
const { container } = renderWithProviders(<NotificationsTab />);
|
||||
|
||||
expect(await screen.findByText('Communication Health')).toBeDefined();
|
||||
expect(screen.getByText('Local Squad Chat')).toBeDefined();
|
||||
expect(screen.getAllByText('Squad Chat Webhook').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText(/Visually verified: not recorded in VK/).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('X-VK-Signature')).toBeDefined();
|
||||
expect(await screen.findByText(/success 202/)).toBeDefined();
|
||||
expect(screen.getByLabelText('Channel')).toBeDefined();
|
||||
expect(screen.getByLabelText('Failure Webhook URL')).toBeDefined();
|
||||
expect(screen.getByRole('combobox', { name: 'Mode' })).toBeDefined();
|
||||
expect(screen.getByLabelText('Webhook URL')).toBeDefined();
|
||||
expect(screen.getByLabelText('Secret (Optional)')).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,120 @@
|
|||
import { Select, TextInput } from '@mantine/core';
|
||||
import {
|
||||
Badge,
|
||||
Code,
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useFeatureSettings, useDebouncedFeatureUpdate } from '@/hooks/useFeatureSettings';
|
||||
import { DEFAULT_FEATURE_SETTINGS } from '@veritas-kanban/shared';
|
||||
import { SettingRow, ToggleRow, SectionHeader, SaveIndicator } from '../shared';
|
||||
import { api, type OutboundDeliveryAttempt, type OutboundEndpointRecord } from '@/lib/api';
|
||||
|
||||
type CommunicationState = 'ok' | 'warn' | 'off' | 'unknown';
|
||||
|
||||
const STATE_COLORS: Record<CommunicationState, string> = {
|
||||
ok: 'green',
|
||||
warn: 'yellow',
|
||||
off: 'gray',
|
||||
unknown: 'blue',
|
||||
};
|
||||
|
||||
function redactDestination(value?: string): string {
|
||||
if (!value?.trim()) return 'not configured';
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
const hasSensitiveParts = parsed.pathname !== '/' || parsed.search || parsed.hash;
|
||||
return hasSensitiveParts ? `${parsed.origin}/[redacted]` : parsed.origin;
|
||||
} catch {
|
||||
return '[redacted destination]';
|
||||
}
|
||||
}
|
||||
|
||||
function formatDelivery(delivery?: OutboundDeliveryAttempt): string {
|
||||
if (!delivery) return 'not recorded';
|
||||
const status = delivery.responseStatus
|
||||
? `${delivery.status} ${delivery.responseStatus}`
|
||||
: delivery.status;
|
||||
return `${status} at ${new Date(delivery.completedAt).toLocaleString()}`;
|
||||
}
|
||||
|
||||
function notificationDestination(settings: { channel?: string; webhookUrl?: string }): string {
|
||||
const channel = settings.channel?.trim();
|
||||
const webhookUrl = settings.webhookUrl?.trim();
|
||||
if (channel && webhookUrl) return `Teams channel + webhook: ${redactDestination(webhookUrl)}`;
|
||||
if (channel) return 'Teams channel configured';
|
||||
if (webhookUrl) return `Webhook: ${redactDestination(webhookUrl)}`;
|
||||
|
||||
return 'not configured';
|
||||
}
|
||||
|
||||
function findDelivery(deliveries: OutboundDeliveryAttempt[], endpointId: string) {
|
||||
return deliveries.find((delivery) => delivery.endpointId === endpointId);
|
||||
}
|
||||
|
||||
function findEndpoint(endpoints: OutboundEndpointRecord[], endpointId: string) {
|
||||
return endpoints.find((endpoint) => endpoint.id === endpointId);
|
||||
}
|
||||
|
||||
function healthState(enabled: boolean, configured: boolean): CommunicationState {
|
||||
if (!enabled) return 'off';
|
||||
return configured ? 'ok' : 'warn';
|
||||
}
|
||||
|
||||
function HealthCard({
|
||||
title,
|
||||
state,
|
||||
label,
|
||||
detail,
|
||||
}: {
|
||||
title: string;
|
||||
state: CommunicationState;
|
||||
label: string;
|
||||
detail: string;
|
||||
}) {
|
||||
return (
|
||||
<Paper withBorder radius="md" p="sm">
|
||||
<Group justify="space-between" gap="sm">
|
||||
<Text size="sm" fw={600}>
|
||||
{title}
|
||||
</Text>
|
||||
<Badge color={STATE_COLORS[state]} variant="light" tt="none">
|
||||
{label}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{detail}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function endpointLabel(endpoint?: OutboundEndpointRecord): string {
|
||||
if (!endpoint) return 'endpoint pending';
|
||||
if (!endpoint.validation.valid) return `blocked: ${endpoint.validation.reason ?? 'invalid URL'}`;
|
||||
return endpoint.enabled ? 'endpoint enabled' : 'endpoint disabled';
|
||||
}
|
||||
|
||||
export function NotificationsTab() {
|
||||
const { settings } = useFeatureSettings();
|
||||
const { debouncedUpdate, isPending } = useDebouncedFeatureUpdate();
|
||||
const { data: outboundEndpoints = [] } = useQuery({
|
||||
queryKey: ['integrations', 'outbound', 'endpoints'],
|
||||
queryFn: api.integrations.outboundEndpoints,
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
});
|
||||
const { data: outboundDeliveries = [] } = useQuery({
|
||||
queryKey: ['integrations', 'outbound', 'deliveries', 25],
|
||||
queryFn: () => api.integrations.outboundDeliveries(25),
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const updateNotifications = (key: string, value: any) => {
|
||||
debouncedUpdate({ notifications: { [key]: value } });
|
||||
|
|
@ -23,9 +132,115 @@ export function NotificationsTab() {
|
|||
};
|
||||
|
||||
const webhookMode = settings.squadWebhook?.mode ?? DEFAULT_FEATURE_SETTINGS.squadWebhook.mode;
|
||||
const notificationsEnabled =
|
||||
settings.notifications?.enabled ?? DEFAULT_FEATURE_SETTINGS.notifications.enabled;
|
||||
const notificationDestinationConfigured = Boolean(
|
||||
settings.notifications?.channel?.trim() || settings.notifications?.webhookUrl?.trim()
|
||||
);
|
||||
const failureWebhookConfigured = Boolean(settings.notifications?.webhookUrl?.trim());
|
||||
const failureAlertsEnabled =
|
||||
notificationsEnabled &&
|
||||
(settings.notifications?.onAgentFailure ??
|
||||
DEFAULT_FEATURE_SETTINGS.notifications.onAgentFailure);
|
||||
const squadWebhookEnabled =
|
||||
settings.squadWebhook?.enabled ?? DEFAULT_FEATURE_SETTINGS.squadWebhook.enabled;
|
||||
const squadDestination =
|
||||
webhookMode === 'openclaw'
|
||||
? settings.squadWebhook?.openclawGatewayUrl
|
||||
: settings.squadWebhook?.url;
|
||||
const squadDestinationConfigured = Boolean(squadDestination?.trim());
|
||||
const squadEndpointId = webhookMode === 'openclaw' ? 'squad.openclawWake' : 'squad.webhook';
|
||||
const squadEndpoint = findEndpoint(outboundEndpoints, squadEndpointId);
|
||||
const squadDelivery = findDelivery(outboundDeliveries, squadEndpointId);
|
||||
const notificationEndpoint = findEndpoint(outboundEndpoints, 'notifications.failureAlert');
|
||||
const failureDelivery = findDelivery(outboundDeliveries, 'notifications.failureAlert');
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<SectionHeader title="Communication Health" />
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xs">
|
||||
<HealthCard
|
||||
title="Local Squad Chat"
|
||||
state="ok"
|
||||
label="Working"
|
||||
detail="Local messages save to the coordination log. External wake or reply behavior requires a configured consumer."
|
||||
/>
|
||||
<HealthCard
|
||||
title="Squad Chat Webhook"
|
||||
state={healthState(squadWebhookEnabled, squadDestinationConfigured)}
|
||||
label={
|
||||
!squadWebhookEnabled
|
||||
? 'Disabled'
|
||||
: squadDestinationConfigured
|
||||
? 'Configured'
|
||||
: 'Missing destination'
|
||||
}
|
||||
detail={`${endpointLabel(squadEndpoint)}. Destination: ${redactDestination(squadDestination)}. HTTP accepted: ${formatDelivery(squadDelivery)}. Visually verified: not recorded in VK.`}
|
||||
/>
|
||||
<HealthCard
|
||||
title="Broad Notifications"
|
||||
state={healthState(notificationsEnabled, notificationDestinationConfigured)}
|
||||
label={
|
||||
!notificationsEnabled
|
||||
? 'Disabled'
|
||||
: notificationDestinationConfigured
|
||||
? 'Configured'
|
||||
: 'Missing destination'
|
||||
}
|
||||
detail={`Destination: ${notificationDestination(settings.notifications ?? {})}. Last test result: not recorded in VK.`}
|
||||
/>
|
||||
<HealthCard
|
||||
title="Failure Alerts"
|
||||
state={healthState(failureAlertsEnabled, failureWebhookConfigured)}
|
||||
label={
|
||||
!failureAlertsEnabled
|
||||
? 'Disabled'
|
||||
: failureWebhookConfigured
|
||||
? 'Webhook configured'
|
||||
: 'Stored only'
|
||||
}
|
||||
detail={`Immediate webhook: ${redactDestination(settings.notifications?.webhookUrl)}. ${endpointLabel(notificationEndpoint)}. HTTP accepted: ${formatDelivery(failureDelivery)}. Visually verified: not recorded in VK.`}
|
||||
/>
|
||||
<HealthCard
|
||||
title="Inbound Wake / Replies"
|
||||
state={
|
||||
webhookMode === 'openclaw' && squadWebhookEnabled && squadDestinationConfigured
|
||||
? 'ok'
|
||||
: 'off'
|
||||
}
|
||||
label={
|
||||
webhookMode === 'openclaw' && squadWebhookEnabled && squadDestinationConfigured
|
||||
? 'OpenClaw configured'
|
||||
: 'Not configured'
|
||||
}
|
||||
detail="Generic webhooks are outbound only. OpenClaw Direct can wake a gateway; replies still require the external orchestrator."
|
||||
/>
|
||||
<Paper withBorder radius="md" p="sm">
|
||||
<Stack gap={4}>
|
||||
<Group justify="space-between" gap="sm">
|
||||
<Text size="sm" fw={600}>
|
||||
Payload & Signing
|
||||
</Text>
|
||||
<Badge color="blue" variant="light" tt="none">
|
||||
Redacted
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
Generic webhooks send <Code>event</Code>, <Code>message.id</Code>,{' '}
|
||||
<Code>message.agent</Code>, <Code>message.message</Code>,{' '}
|
||||
<Code>message.timestamp</Code>, and <Code>isHuman</Code>. HMAC signatures use{' '}
|
||||
<Code>X-VK-Signature</Code> when a secret is set. OpenClaw Direct posts a wake
|
||||
payload to <Code>/tools/invoke</Code> with bearer auth. Secrets and bearer tokens
|
||||
are not shown after save.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
<div className="border-t my-6" />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<SectionHeader title="Notifications" onReset={resetNotifications} />
|
||||
<SaveIndicator isPending={isPending} />
|
||||
|
|
@ -80,6 +295,20 @@ export function NotificationsTab() {
|
|||
w={192}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="Failure Webhook URL"
|
||||
description="Optional Teams or generic webhook for immediate failure alert delivery"
|
||||
>
|
||||
<TextInput
|
||||
value={settings.notifications?.webhookUrl ?? ''}
|
||||
onChange={(e) => updateNotifications('webhookUrl', e.target.value || undefined)}
|
||||
placeholder="https://example.com/webhook"
|
||||
aria-label="Failure Webhook URL"
|
||||
size="xs"
|
||||
w={384}
|
||||
type="url"
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { tracesApi } from './traces';
|
|||
import { maintenanceApi } from './maintenance';
|
||||
import { skillCapabilitiesApi } from './skill-capabilities';
|
||||
import { skillSecurityApi } from './skill-security';
|
||||
import { integrationsApi } from './integrations';
|
||||
|
||||
// Assemble the full API object (matches original structure exactly)
|
||||
export const api = {
|
||||
|
|
@ -52,6 +53,7 @@ export const api = {
|
|||
workProducts: workProductsApi,
|
||||
traces: tracesApi,
|
||||
maintenance: maintenanceApi,
|
||||
integrations: integrationsApi,
|
||||
skillCapabilities: skillCapabilitiesApi,
|
||||
skillSecurity: skillSecurityApi,
|
||||
};
|
||||
|
|
@ -69,6 +71,12 @@ export type {
|
|||
export type { WorkProductExportFormat, WorkProductExportOptions } from './work-products';
|
||||
export type { TraceStatus } from './traces';
|
||||
export type { SqlitePortabilityReport } from './maintenance';
|
||||
export type {
|
||||
OutboundDeliveryAttempt,
|
||||
OutboundDeliveryStatus,
|
||||
OutboundEndpointRecord,
|
||||
OutboundEndpointType,
|
||||
} from './integrations';
|
||||
|
||||
// Re-export managed list helper
|
||||
export { managedList } from './managed-list';
|
||||
|
|
|
|||
65
web/src/lib/api/integrations.ts
Normal file
65
web/src/lib/api/integrations.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { API_BASE, handleResponse } from './helpers';
|
||||
|
||||
export type OutboundEndpointType =
|
||||
| 'broadcast-webhook'
|
||||
| 'lifecycle-hook-webhook'
|
||||
| 'transition-hook-webhook'
|
||||
| 'policy-webhook'
|
||||
| 'squad-webhook'
|
||||
| 'openclaw-wake'
|
||||
| 'openclaw-gateway'
|
||||
| 'failure-alert-webhook';
|
||||
|
||||
export type OutboundDeliveryStatus = 'success' | 'failed' | 'blocked' | 'timeout' | 'skipped';
|
||||
|
||||
export interface OutboundEndpointRecord {
|
||||
id: string;
|
||||
type: OutboundEndpointType;
|
||||
displayName: string;
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
auth: {
|
||||
type: 'none' | 'hmac-sha256' | 'bearer' | 'custom-header';
|
||||
secretRef?: string;
|
||||
headerName?: string;
|
||||
hasSecret?: boolean;
|
||||
};
|
||||
validation: {
|
||||
valid: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface OutboundDeliveryAttempt {
|
||||
id: string;
|
||||
endpointId: string;
|
||||
endpointType: OutboundEndpointType;
|
||||
displayName: string;
|
||||
method: string;
|
||||
sanitizedUrl: string;
|
||||
status: OutboundDeliveryStatus;
|
||||
responseStatus?: number;
|
||||
responseClass?: string;
|
||||
durationMs: number;
|
||||
attempt: number;
|
||||
error?: string;
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export const integrationsApi = {
|
||||
outboundEndpoints: async (): Promise<OutboundEndpointRecord[]> => {
|
||||
const response = await fetch(`${API_BASE}/integrations/outbound/endpoints`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<OutboundEndpointRecord[]>(response);
|
||||
},
|
||||
|
||||
outboundDeliveries: async (limit = 25): Promise<OutboundDeliveryAttempt[]> => {
|
||||
const response = await fetch(`${API_BASE}/integrations/outbound/deliveries?limit=${limit}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<OutboundDeliveryAttempt[]>(response);
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue