@burdenoff/microfe-movethewheels 2026.1031.5 → 2026.1101.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,752 @@
1
+ /**
2
+ * Integrations Page (BOFF-4404)
3
+ *
4
+ * Connect external carrier, ELD and GPS feeds to MoveTheWheels. Each
5
+ * integration is a signed webhook: the sender posts tracking updates to the
6
+ * integration's URL, signed with HMAC-SHA256 over the raw body, and every
7
+ * accepted update becomes a tracking event on the matching shipment.
8
+ *
9
+ * The signing secret is generated in the browser, shown exactly once and never
10
+ * read back from the service. Named carrier networks (project44, FourKites,
11
+ * Descartes) are listed as not yet available: they need vendor agreements
12
+ * before an adapter can ship, and the service refuses them.
13
+ */
14
+ import { useEffect, useRef, useState, type FormEvent } from 'react';
15
+ import {
16
+ Activity,
17
+ Check,
18
+ ChevronDown,
19
+ ChevronUp,
20
+ Copy,
21
+ KeyRound,
22
+ Pause,
23
+ Play,
24
+ Plug,
25
+ Plus,
26
+ RefreshCw,
27
+ ShieldCheck,
28
+ Trash2,
29
+ } from 'lucide-react';
30
+ import {
31
+ CTAOverflowMenu,
32
+ PagePurpose,
33
+ ResponsiveDialog,
34
+ ResponsiveDialogContent,
35
+ ResponsiveDialogDescription,
36
+ ResponsiveDialogFooter,
37
+ ResponsiveDialogHeader,
38
+ ResponsiveDialogTitle,
39
+ } from '@burdenoff/fe-libs/ui';
40
+ import { PageLayout } from '../components/PageLayout';
41
+ import { MetricCard, MetricGrid } from '../components/MetricCard';
42
+ import { Notice, useNotice } from '../components/Notice';
43
+ import { FormError } from '../components/FormError';
44
+ import {
45
+ Badge,
46
+ Button,
47
+ Card,
48
+ CardContent,
49
+ Input,
50
+ Label,
51
+ Select,
52
+ SelectContent,
53
+ SelectItem,
54
+ SelectTrigger,
55
+ SelectValue,
56
+ Skeleton,
57
+ } from '../components/ui';
58
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
59
+ import {
60
+ useCarrierIntegrationActions,
61
+ useCarrierIntegrationCounts,
62
+ useCarrierIntegrationEvents,
63
+ useCarrierIntegrations,
64
+ } from '../hooks/useCarrierIntegrations';
65
+ import {
66
+ INTEGRATION_STATUSES,
67
+ MAX_UPDATES_PER_DELIVERY,
68
+ ROADMAP_PROVIDERS,
69
+ SIGNATURE_HEADER,
70
+ TRACKING_EVENT_TYPES,
71
+ curlExample,
72
+ eventStatusTone,
73
+ examplePayload,
74
+ generateSigningSecret,
75
+ humanize,
76
+ integrationNameError,
77
+ integrationStatusTone,
78
+ signingSecretError,
79
+ statusExplanation,
80
+ webhookUrl,
81
+ type CarrierIntegration,
82
+ type IntegrationStatus,
83
+ } from '../utils/carrierIntegrations';
84
+ import { formatDateTime } from '../utils/formatters';
85
+
86
+ const PAGE_SIZE = 20;
87
+ const ALL = 'ALL';
88
+
89
+ function errorMessage(error: unknown, fallback: string): string {
90
+ return error instanceof Error && error.message ? error.message : fallback;
91
+ }
92
+
93
+ function CopyField({
94
+ label,
95
+ value,
96
+ multiline,
97
+ }: {
98
+ label: string;
99
+ value: string;
100
+ multiline?: boolean;
101
+ }) {
102
+ const [copied, setCopied] = useState(false);
103
+ const copy = async () => {
104
+ try {
105
+ await navigator.clipboard.writeText(value);
106
+ setCopied(true);
107
+ window.setTimeout(() => setCopied(false), 2000);
108
+ } catch {
109
+ setCopied(false);
110
+ }
111
+ };
112
+ return (
113
+ <div className="space-y-1">
114
+ <div className="flex items-center justify-between gap-2">
115
+ <span className="text-xs font-medium text-text-secondary">{label}</span>
116
+ <Button
117
+ type="button"
118
+ size="sm"
119
+ variant="ghost"
120
+ onClick={() => void copy()}
121
+ aria-label={`Copy ${label.toLowerCase()}`}
122
+ >
123
+ {copied ? <Check size={14} className="mr-1" /> : <Copy size={14} className="mr-1" />}
124
+ {copied ? 'Copied' : 'Copy'}
125
+ </Button>
126
+ </div>
127
+ {multiline ? (
128
+ <pre className="max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-lg bg-bg-sunken px-3 py-2 font-mono text-xs text-text-primary">
129
+ {value}
130
+ </pre>
131
+ ) : (
132
+ <code className="block break-all rounded-lg bg-bg-sunken px-3 py-2 font-mono text-xs text-text-primary">
133
+ {value}
134
+ </code>
135
+ )}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ /** Shown once after a secret is generated; the service never returns it again. */
141
+ function SecretRevealDialog({
142
+ secret,
143
+ integrationName,
144
+ onClose,
145
+ }: {
146
+ secret: string | null;
147
+ integrationName: string;
148
+ onClose: () => void;
149
+ }) {
150
+ return (
151
+ <ResponsiveDialog open={secret !== null} onOpenChange={(open) => !open && onClose()}>
152
+ <ResponsiveDialogContent>
153
+ <ResponsiveDialogHeader>
154
+ <ResponsiveDialogTitle>Signing secret for {integrationName}</ResponsiveDialogTitle>
155
+ <ResponsiveDialogDescription>
156
+ Copy it into the sending system now. MoveTheWheels stores it to verify deliveries but
157
+ will not show it again; rotate it if it is lost.
158
+ </ResponsiveDialogDescription>
159
+ </ResponsiveDialogHeader>
160
+ {secret ? <CopyField key={secret} label="Signing secret" value={secret} /> : null}
161
+ <ResponsiveDialogFooter>
162
+ <Button onClick={onClose}>I have saved it</Button>
163
+ </ResponsiveDialogFooter>
164
+ </ResponsiveDialogContent>
165
+ </ResponsiveDialog>
166
+ );
167
+ }
168
+
169
+ function CreateIntegrationDialog({
170
+ open,
171
+ onOpenChange,
172
+ onCreated,
173
+ }: {
174
+ open: boolean;
175
+ onOpenChange: (open: boolean) => void;
176
+ onCreated: (integration: CarrierIntegration, secret: string) => void;
177
+ }) {
178
+ const actions = useCarrierIntegrationActions();
179
+ const [name, setName] = useState('');
180
+ const [secret, setSecret] = useState(() => generateSigningSecret());
181
+ const [error, setError] = useState<string | null>(null);
182
+ const [isSaving, setIsSaving] = useState(false);
183
+
184
+ const reset = () => {
185
+ setName('');
186
+ setSecret(generateSigningSecret());
187
+ setError(null);
188
+ };
189
+
190
+ const submit = async (event: FormEvent) => {
191
+ event.preventDefault();
192
+ const problem = integrationNameError(name) ?? signingSecretError(secret);
193
+ if (problem) {
194
+ setError(problem);
195
+ return;
196
+ }
197
+ setIsSaving(true);
198
+ setError(null);
199
+ try {
200
+ const created = await actions.create({
201
+ provider: 'GENERIC_WEBHOOK',
202
+ name,
203
+ apiSecret: secret,
204
+ });
205
+ onCreated(created, secret);
206
+ reset();
207
+ onOpenChange(false);
208
+ } catch (createError) {
209
+ setError(errorMessage(createError, 'Could not create the integration.'));
210
+ } finally {
211
+ setIsSaving(false);
212
+ }
213
+ };
214
+
215
+ return (
216
+ <ResponsiveDialog
217
+ open={open}
218
+ onOpenChange={(next) => {
219
+ if (!next) reset();
220
+ onOpenChange(next);
221
+ }}
222
+ >
223
+ <ResponsiveDialogContent>
224
+ <form onSubmit={(event) => void submit(event)} className="space-y-4">
225
+ <ResponsiveDialogHeader>
226
+ <ResponsiveDialogTitle>Connect a tracking feed</ResponsiveDialogTitle>
227
+ <ResponsiveDialogDescription>
228
+ A signed webhook for a carrier, ELD or GPS provider. Updates it sends become tracking
229
+ events on the matching shipments.
230
+ </ResponsiveDialogDescription>
231
+ </ResponsiveDialogHeader>
232
+ <div className="space-y-1.5">
233
+ <Label htmlFor="integration-name">Name</Label>
234
+ <Input
235
+ id="integration-name"
236
+ value={name}
237
+ maxLength={120}
238
+ placeholder="e.g. Fleet GPS feed"
239
+ onChange={(event) => setName(event.target.value)}
240
+ autoFocus
241
+ />
242
+ </div>
243
+ <div className="space-y-1.5">
244
+ <Label>Type</Label>
245
+ <p className="rounded-lg border border-border-default px-3 py-2 text-sm">
246
+ Signed webhook (any carrier, ELD or GPS system that can send HTTPS POSTs)
247
+ </p>
248
+ </div>
249
+ <div className="space-y-1.5">
250
+ <div className="flex items-center justify-between">
251
+ <Label>Signing secret</Label>
252
+ <Button
253
+ type="button"
254
+ size="sm"
255
+ variant="ghost"
256
+ onClick={() => setSecret(generateSigningSecret())}
257
+ >
258
+ <RefreshCw size={14} className="mr-1" />
259
+ Generate another
260
+ </Button>
261
+ </div>
262
+ <code className="block break-all rounded-lg bg-bg-sunken px-3 py-2 font-mono text-xs">
263
+ {secret}
264
+ </code>
265
+ <p className="text-xs text-text-secondary">
266
+ Generated in your browser. You can copy it on the next step.
267
+ </p>
268
+ </div>
269
+ <FormError message={error} />
270
+ <ResponsiveDialogFooter>
271
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
272
+ Cancel
273
+ </Button>
274
+ <Button type="submit" disabled={isSaving}>
275
+ {isSaving ? 'Connecting…' : 'Connect'}
276
+ </Button>
277
+ </ResponsiveDialogFooter>
278
+ </form>
279
+ </ResponsiveDialogContent>
280
+ </ResponsiveDialog>
281
+ );
282
+ }
283
+
284
+ function DeliveryLog({ integrationId, refreshKey }: { integrationId: string; refreshKey: number }) {
285
+ const { events, isLoading, error } = useCarrierIntegrationEvents(integrationId, refreshKey);
286
+ return (
287
+ <div className="space-y-2">
288
+ <p className="text-sm font-medium">Recent deliveries</p>
289
+ <FormError message={error?.message} />
290
+ {isLoading && events.length === 0 ? (
291
+ <Skeleton className="h-12 w-full" />
292
+ ) : events.length === 0 ? (
293
+ <p className="text-sm text-text-secondary">
294
+ No deliveries yet. Send a signed test update with the example below.
295
+ </p>
296
+ ) : (
297
+ <ul className="space-y-2">
298
+ {events.map((event) => (
299
+ <li
300
+ key={event.id}
301
+ className="flex flex-wrap items-start justify-between gap-3 rounded-lg bg-bg-sunken px-3 py-2"
302
+ >
303
+ <div className="min-w-0">
304
+ <p className="text-sm font-medium">
305
+ {event.eventType ? humanize(event.eventType) : 'Delivery'}
306
+ {event.externalEventId ? (
307
+ <span className="ml-2 font-mono text-xs text-text-tertiary">
308
+ {event.externalEventId}
309
+ </span>
310
+ ) : null}
311
+ </p>
312
+ <p className="text-xs text-text-secondary">{formatDateTime(event.receivedAt)}</p>
313
+ {event.errorMessage ? (
314
+ <p className="mt-1 break-words text-xs text-status-error-text">
315
+ {event.errorMessage}
316
+ </p>
317
+ ) : null}
318
+ </div>
319
+ <Badge variant={eventStatusTone(event.status)}>{humanize(event.status)}</Badge>
320
+ </li>
321
+ ))}
322
+ </ul>
323
+ )}
324
+ </div>
325
+ );
326
+ }
327
+
328
+ function SetupInstructions({ url }: { url: string }) {
329
+ return (
330
+ <div className="space-y-3">
331
+ <p className="text-sm font-medium">How to send updates</p>
332
+ <CopyField label="Webhook URL" value={url} />
333
+ <ul className="list-disc space-y-1 pl-5 text-xs text-text-secondary">
334
+ <li>POST JSON: one update object, or an array of up to {MAX_UPDATES_PER_DELIVERY}.</li>
335
+ <li>
336
+ Sign the exact raw body with HMAC-SHA256 using the signing secret and send the hex digest
337
+ in the <code className="font-mono">{SIGNATURE_HEADER}</code> header (a{' '}
338
+ <code className="font-mono">sha256=</code> prefix is accepted).
339
+ </li>
340
+ <li>
341
+ Identify the shipment with <code className="font-mono">trackingNumber</code> or its{' '}
342
+ <code className="font-mono">shipmentId</code>. Event types:{' '}
343
+ {TRACKING_EVENT_TYPES.join(', ')}.
344
+ </li>
345
+ <li>
346
+ Send a stable <code className="font-mono">externalEventId</code> so retries are recorded
347
+ once instead of twice.
348
+ </li>
349
+ </ul>
350
+ <CopyField label="Example body" value={examplePayload()} multiline />
351
+ <CopyField label="Example request" value={curlExample(url)} multiline />
352
+ </div>
353
+ );
354
+ }
355
+
356
+ function IntegrationCard({
357
+ integration,
358
+ url,
359
+ expanded,
360
+ refreshKey,
361
+ busy,
362
+ onToggle,
363
+ onSetStatus,
364
+ onRotate,
365
+ onDelete,
366
+ }: {
367
+ integration: CarrierIntegration;
368
+ url: string;
369
+ expanded: boolean;
370
+ refreshKey: number;
371
+ /** An action on this integration is in flight; its other actions wait. */
372
+ busy: boolean;
373
+ onToggle: () => void;
374
+ onSetStatus: (status: IntegrationStatus) => void;
375
+ onRotate: () => void;
376
+ onDelete: () => void;
377
+ }) {
378
+ const isActive = integration.status === 'ACTIVE';
379
+ return (
380
+ <Card>
381
+ <CardContent className="p-5">
382
+ <div className="flex flex-wrap items-start justify-between gap-3">
383
+ <div className="min-w-0 space-y-1">
384
+ <div className="flex flex-wrap items-center gap-2">
385
+ <p className="text-base font-semibold">{integration.name}</p>
386
+ <Badge variant={integrationStatusTone(integration.status)}>
387
+ {humanize(integration.status)}
388
+ </Badge>
389
+ </div>
390
+ <p className="text-xs text-text-secondary">
391
+ Signed webhook · {statusExplanation(integration.status)}
392
+ </p>
393
+ <p className="text-xs text-text-secondary">
394
+ Last accepted update:{' '}
395
+ {integration.lastSyncAt ? formatDateTime(integration.lastSyncAt) : 'none yet'}
396
+ {integration.lastErrorAt
397
+ ? ` · Last error ${formatDateTime(integration.lastErrorAt)}`
398
+ : ''}
399
+ </p>
400
+ {integration.lastErrorMessage ? (
401
+ <p className="break-words text-xs text-status-error-text">
402
+ {integration.lastErrorMessage}
403
+ </p>
404
+ ) : null}
405
+ </div>
406
+ <div className="flex flex-wrap items-center gap-2">
407
+ <Button
408
+ size="sm"
409
+ variant="outline"
410
+ disabled={busy}
411
+ onClick={() => onSetStatus(isActive ? 'PAUSED' : 'ACTIVE')}
412
+ >
413
+ {isActive ? (
414
+ <Pause size={14} className="mr-1" />
415
+ ) : (
416
+ <Play size={14} className="mr-1" />
417
+ )}
418
+ {isActive ? 'Pause' : 'Resume'}
419
+ </Button>
420
+ <Button size="sm" variant="outline" disabled={busy} onClick={onRotate}>
421
+ <KeyRound size={14} className="mr-1" />
422
+ Rotate secret
423
+ </Button>
424
+ <Button
425
+ size="sm"
426
+ variant="ghost"
427
+ disabled={busy}
428
+ onClick={onDelete}
429
+ aria-label={`Delete ${integration.name}`}
430
+ >
431
+ <Trash2 size={14} />
432
+ </Button>
433
+ <Button
434
+ size="sm"
435
+ variant="ghost"
436
+ onClick={onToggle}
437
+ aria-expanded={expanded}
438
+ aria-label={expanded ? 'Hide details' : 'Show details'}
439
+ >
440
+ {expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
441
+ </Button>
442
+ </div>
443
+ </div>
444
+ {expanded ? (
445
+ <div className="mt-4 grid gap-6 border-t border-border-subtle pt-4 lg:grid-cols-2">
446
+ <SetupInstructions url={url} />
447
+ <DeliveryLog integrationId={integration.id} refreshKey={refreshKey} />
448
+ </div>
449
+ ) : null}
450
+ </CardContent>
451
+ </Card>
452
+ );
453
+ }
454
+
455
+ /**
456
+ * Remounts the page whenever the host switches tenant or workspace, so no
457
+ * server state, filter, page, pending reveal or in-flight mutation from the
458
+ * previous workspace survives into the next one.
459
+ */
460
+ export default function IntegrationsPage() {
461
+ const { tenantId, workspaceId } = useMoveTheWheels();
462
+ return <WorkspaceIntegrationsPage key={`${tenantId ?? ''}:${workspaceId ?? ''}`} />;
463
+ }
464
+
465
+ function WorkspaceIntegrationsPage() {
466
+ const { apiGatewayUrl } = useMoveTheWheels();
467
+ const actions = useCarrierIntegrationActions();
468
+ const { message: notice, notify } = useNotice();
469
+ const [actionError, setActionError] = useState<string | null>(null);
470
+ const [status, setStatus] = useState<IntegrationStatus | typeof ALL>(ALL);
471
+ const [page, setPage] = useState(0);
472
+ const [refreshKey, setRefreshKey] = useState(0);
473
+ const [expandedId, setExpandedId] = useState<string | null>(null);
474
+ const [createOpen, setCreateOpen] = useState(false);
475
+ // Secrets waiting to be shown, oldest first. Each is shown once and cannot be
476
+ // read back, so concurrent rotations on different feeds queue instead of
477
+ // replacing an unacknowledged secret.
478
+ const [reveals, setReveals] = useState<{ name: string; secret: string }[]>([]);
479
+ const revealed = reveals[0] ?? null;
480
+ const reveal = (name: string, secret: string) =>
481
+ setReveals((queue) => [...queue, { name, secret }]);
482
+ // One write per integration at a time: two overlapping secret rotations
483
+ // could otherwise reveal a secret the server has already replaced.
484
+ const busyRef = useRef(new Set<string>());
485
+ const [busyIds, setBusyIds] = useState<ReadonlySet<string>>(() => new Set());
486
+ const exclusive = async (id: string, action: () => Promise<void>) => {
487
+ if (busyRef.current.has(id)) return;
488
+ busyRef.current.add(id);
489
+ setBusyIds(new Set(busyRef.current));
490
+ try {
491
+ await action();
492
+ } finally {
493
+ busyRef.current.delete(id);
494
+ setBusyIds(new Set(busyRef.current));
495
+ }
496
+ };
497
+
498
+ const { integrations, total, isLoading, error, refetch } = useCarrierIntegrations({
499
+ status: status === ALL ? undefined : status,
500
+ limit: PAGE_SIZE,
501
+ offset: page * PAGE_SIZE,
502
+ });
503
+ // Workspace-wide totals, independent of the status filter and page.
504
+ const {
505
+ all: allFeeds,
506
+ active: activeFeeds,
507
+ refetch: refetchCounts,
508
+ } = useCarrierIntegrationCounts();
509
+ const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
510
+ // A delete or pause can empty the last page; step back to the last page
511
+ // that still has feeds instead of showing a false empty state.
512
+ useEffect(() => {
513
+ if (!isLoading && page > 0 && page >= pageCount) setPage(pageCount - 1);
514
+ }, [isLoading, page, pageCount]);
515
+ const syncTimes = integrations
516
+ .map((integration) => integration.lastSyncAt)
517
+ .filter((value): value is string => Boolean(value))
518
+ .sort();
519
+ const lastSync = syncTimes.length > 0 ? syncTimes[syncTimes.length - 1] : undefined;
520
+
521
+ const urlFor = (integration: CarrierIntegration) =>
522
+ // The row's own workspace, never the host's current one: the URL must
523
+ // address the workspace that owns the integration.
524
+ webhookUrl(apiGatewayUrl, integration.workspaceId, integration.id, window.location.origin);
525
+
526
+ const refresh = () => {
527
+ setRefreshKey((key) => key + 1);
528
+ void refetch();
529
+ void refetchCounts();
530
+ };
531
+ const done = (message: string) => {
532
+ setActionError(null);
533
+ notify(message);
534
+ refresh();
535
+ };
536
+
537
+ const setIntegrationStatus = async (integration: CarrierIntegration, next: IntegrationStatus) => {
538
+ try {
539
+ await actions.update(integration.id, { status: next });
540
+ done(next === 'ACTIVE' ? `Resumed ${integration.name}.` : `Paused ${integration.name}.`);
541
+ } catch (updateError) {
542
+ setActionError(errorMessage(updateError, 'Could not change the integration status.'));
543
+ }
544
+ };
545
+
546
+ const rotateSecret = async (integration: CarrierIntegration) => {
547
+ if (
548
+ !window.confirm(
549
+ `Rotate the signing secret for ${integration.name}? Deliveries signed with the old secret are refused as soon as it changes.`
550
+ )
551
+ )
552
+ return;
553
+ const secret = generateSigningSecret();
554
+ try {
555
+ await actions.update(integration.id, { apiSecret: secret });
556
+ reveal(integration.name, secret);
557
+ done(`Rotated the signing secret for ${integration.name}.`);
558
+ } catch (rotateError) {
559
+ setActionError(errorMessage(rotateError, 'Could not rotate the signing secret.'));
560
+ }
561
+ };
562
+
563
+ const removeIntegration = async (integration: CarrierIntegration) => {
564
+ if (
565
+ !window.confirm(
566
+ `Delete ${integration.name}? Its webhook URL stops working; tracking events it already created stay on their shipments.`
567
+ )
568
+ )
569
+ return;
570
+ try {
571
+ await actions.remove(integration.id);
572
+ if (expandedId === integration.id) setExpandedId(null);
573
+ done(`Deleted ${integration.name}.`);
574
+ } catch (removeError) {
575
+ setActionError(errorMessage(removeError, 'Could not delete the integration.'));
576
+ }
577
+ };
578
+
579
+ return (
580
+ <PageLayout
581
+ title="Integrations"
582
+ subtitle="Carrier, ELD and GPS tracking feeds"
583
+ icon={<Plug size={24} />}
584
+ headerActions={
585
+ <CTAOverflowMenu
586
+ primary={{
587
+ label: 'Connect feed',
588
+ icon: <Plus size={16} />,
589
+ onSelect: () => setCreateOpen(true),
590
+ }}
591
+ actions={[{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: refresh }]}
592
+ />
593
+ }
594
+ >
595
+ <PagePurpose icon={<Plug size={18} />}>
596
+ Connect the systems that already know where your freight is. Each feed gets its own signed
597
+ webhook; every update it sends becomes a tracking event on the matching shipment, so status
598
+ and ETA stay current without manual entry.
599
+ </PagePurpose>
600
+
601
+ <Notice message={notice} />
602
+ <FormError message={actionError} />
603
+
604
+ <MetricGrid columns={3}>
605
+ <MetricCard label="Feeds" value={allFeeds} icon={<Plug size={20} />} accent="brand" />
606
+ <MetricCard
607
+ label="Active"
608
+ value={activeFeeds}
609
+ icon={<ShieldCheck size={20} />}
610
+ accent="success"
611
+ />
612
+ <MetricCard
613
+ label="Last accepted update (shown)"
614
+ value={lastSync ? formatDateTime(lastSync) : '—'}
615
+ icon={<Activity size={20} />}
616
+ accent="blue"
617
+ />
618
+ </MetricGrid>
619
+
620
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
621
+ <Select
622
+ value={status}
623
+ onValueChange={(value) => {
624
+ setStatus(value as IntegrationStatus | typeof ALL);
625
+ setPage(0);
626
+ }}
627
+ >
628
+ <SelectTrigger aria-label="Status" className="sm:w-56">
629
+ <SelectValue />
630
+ </SelectTrigger>
631
+ <SelectContent>
632
+ <SelectItem value={ALL}>All statuses</SelectItem>
633
+ {INTEGRATION_STATUSES.map((option) => (
634
+ <SelectItem key={option} value={option}>
635
+ {humanize(option)}
636
+ </SelectItem>
637
+ ))}
638
+ </SelectContent>
639
+ </Select>
640
+ </div>
641
+
642
+ <FormError message={error?.message} />
643
+
644
+ {isLoading && integrations.length === 0 ? (
645
+ <div className="grid gap-4">
646
+ <Skeleton className="h-28 w-full" />
647
+ <Skeleton className="h-28 w-full" />
648
+ </div>
649
+ ) : integrations.length === 0 && !error ? (
650
+ <Card>
651
+ <CardContent className="flex flex-col items-center gap-3 p-10 text-center">
652
+ <Plug size={32} className="text-text-tertiary" />
653
+ <p className="font-medium">
654
+ {status === ALL ? 'No tracking feeds connected' : 'No feeds with this status'}
655
+ </p>
656
+ <p className="max-w-md text-sm text-text-secondary">
657
+ {status === ALL
658
+ ? 'Connect a carrier portal, ELD or GPS tracker so shipment updates arrive on their own.'
659
+ : 'Choose another status to see the rest of your feeds.'}
660
+ </p>
661
+ {status === ALL ? (
662
+ <Button onClick={() => setCreateOpen(true)}>
663
+ <Plus size={16} className="mr-1.5" />
664
+ Connect feed
665
+ </Button>
666
+ ) : null}
667
+ </CardContent>
668
+ </Card>
669
+ ) : (
670
+ <div className="grid gap-4">
671
+ {integrations.map((integration) => (
672
+ <IntegrationCard
673
+ key={integration.id}
674
+ integration={integration}
675
+ url={urlFor(integration)}
676
+ expanded={expandedId === integration.id}
677
+ refreshKey={refreshKey}
678
+ busy={busyIds.has(integration.id)}
679
+ onToggle={() =>
680
+ setExpandedId((id) => (id === integration.id ? null : integration.id))
681
+ }
682
+ onSetStatus={(next) =>
683
+ void exclusive(integration.id, () => setIntegrationStatus(integration, next))
684
+ }
685
+ onRotate={() => void exclusive(integration.id, () => rotateSecret(integration))}
686
+ onDelete={() => void exclusive(integration.id, () => removeIntegration(integration))}
687
+ />
688
+ ))}
689
+ </div>
690
+ )}
691
+
692
+ {total > PAGE_SIZE ? (
693
+ <div className="flex items-center justify-between text-sm">
694
+ <span className="text-text-secondary">
695
+ Page {page + 1} of {pageCount} · {total} feeds
696
+ </span>
697
+ <div className="flex gap-2">
698
+ <Button
699
+ variant="outline"
700
+ size="sm"
701
+ disabled={page === 0}
702
+ onClick={() => setPage((p) => p - 1)}
703
+ >
704
+ Previous
705
+ </Button>
706
+ <Button
707
+ variant="outline"
708
+ size="sm"
709
+ disabled={page + 1 >= pageCount}
710
+ onClick={() => setPage((p) => p + 1)}
711
+ >
712
+ Next
713
+ </Button>
714
+ </div>
715
+ </div>
716
+ ) : null}
717
+
718
+ <Card>
719
+ <CardContent className="space-y-2 p-5">
720
+ <p className="text-sm font-medium">Carrier networks</p>
721
+ <p className="text-sm text-text-secondary">
722
+ Direct connections to {ROADMAP_PROVIDERS.map((provider) => provider.label).join(', ')}{' '}
723
+ are not available yet. Until they are, any carrier or telematics system that can send
724
+ HTTPS webhooks connects through a signed feed above.
725
+ </p>
726
+ <div className="flex flex-wrap gap-2">
727
+ {ROADMAP_PROVIDERS.map((provider) => (
728
+ <Badge key={provider.id} variant="outline">
729
+ {provider.label} · not available yet
730
+ </Badge>
731
+ ))}
732
+ </div>
733
+ </CardContent>
734
+ </Card>
735
+
736
+ <CreateIntegrationDialog
737
+ open={createOpen}
738
+ onOpenChange={setCreateOpen}
739
+ onCreated={(integration, secret) => {
740
+ setExpandedId(integration.id);
741
+ reveal(integration.name, secret);
742
+ done(`Connected ${integration.name}.`);
743
+ }}
744
+ />
745
+ <SecretRevealDialog
746
+ secret={revealed?.secret ?? null}
747
+ integrationName={revealed?.name ?? ''}
748
+ onClose={() => setReveals((queue) => queue.slice(1))}
749
+ />
750
+ </PageLayout>
751
+ );
752
+ }