@burdenoff/microfe-movethewheels 2026.1011.1 → 2026.1011.2

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.
Files changed (30) hide show
  1. package/dist/MoveTheWheelsRoot.js +5 -0
  2. package/dist/MoveTheWheelsRoutes.js +54 -50
  3. package/dist/generated/shipment-status-authority-operations.d.ts +4473 -0
  4. package/dist/hooks/useEpod.js +85 -77
  5. package/dist/hooks/useShipmentStatusAuthority.js +127 -0
  6. package/dist/hooks/useTracking.js +15 -14
  7. package/dist/hooks/useTrackingEventQuality.js +43 -17
  8. package/dist/movethewheels/hooks/useEpod.d.ts +23 -0
  9. package/dist/movethewheels/hooks/useShipmentStatusAuthority.d.ts +23 -0
  10. package/dist/movethewheels/hooks/useTrackingEventQuality.d.ts +5 -0
  11. package/dist/movethewheels/pages/ShipmentStatusAuthorityPage.d.ts +1 -0
  12. package/dist/movethewheels/utils/shipmentStatusDraft.d.ts +45 -0
  13. package/dist/pages/DeliveryCapturePage.js +181 -116
  14. package/dist/pages/ShipmentStatusAuthorityPage.js +563 -0
  15. package/dist/pages/TrackingEventQualityPage.js +6 -1
  16. package/dist/src/generated/shipment-status-authority-operations.js +86 -0
  17. package/dist/utils/shipmentStatusDraft.js +41 -0
  18. package/package.json +5 -4
  19. package/src/generated/shipment-status-authority-operations.ts +5503 -0
  20. package/src/movethewheels/MoveTheWheelsRoot.tsx +6 -0
  21. package/src/movethewheels/MoveTheWheelsRoutes.tsx +2 -0
  22. package/src/movethewheels/hooks/useEpod.ts +47 -12
  23. package/src/movethewheels/hooks/useShipmentStatusAuthority.ts +179 -0
  24. package/src/movethewheels/hooks/useTracking.ts +27 -12
  25. package/src/movethewheels/hooks/useTrackingEventQuality.ts +42 -0
  26. package/src/movethewheels/pages/DeliveryCapturePage.tsx +113 -6
  27. package/src/movethewheels/pages/ShipmentStatusAuthorityPage.tsx +639 -0
  28. package/src/movethewheels/pages/TrackingEventQualityPage.tsx +13 -2
  29. package/src/movethewheels/utils/shipmentStatusDraft.ts +118 -0
  30. package/src/operations/shipment-status-authority/status.graphql +80 -0
@@ -0,0 +1,639 @@
1
+ import { useState } from 'react';
2
+ import { ClipboardList } from 'lucide-react';
3
+ import { PermissionProvider, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
4
+ import { PageLayout } from '../components/PageLayout';
5
+ import { Button, Input, Label, Textarea } from '../components/ui';
6
+ import { reuseCard, reuseSelect, ReuseState } from '../components/ReuseControls';
7
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
8
+ import {
9
+ useShipmentStatusCommand,
10
+ useShipmentStatusQuery,
11
+ } from '../hooks/useShipmentStatusAuthority';
12
+ import {
13
+ shipmentSourceInput,
14
+ sourceStatusAcknowledged,
15
+ manualStatusAcknowledged,
16
+ statusDecisionReason,
17
+ type ShipmentSourceDraft,
18
+ } from '../utils/shipmentStatusDraft';
19
+ import {
20
+ StatusAuthorityShipmentsDocument,
21
+ StatusAuthorityDetailDocument,
22
+ StatusAuthoritySourcesDocument,
23
+ StatusAuthorityConfigureDocument,
24
+ StatusAuthorityManualDocument,
25
+ } from '../../generated/shipment-status-authority-operations';
26
+ import type {
27
+ StatusAuthorityShipmentsQuery,
28
+ StatusAuthorityDetailQuery,
29
+ StatusAuthoritySourcesQuery,
30
+ StatusAuthorityConfigureMutation,
31
+ StatusAuthorityManualMutation,
32
+ ShipmentStatus,
33
+ } from '../../generated/shipment-status-authority-operations';
34
+
35
+ const LIMIT = 20;
36
+ const STATUSES: ShipmentStatus[] = [
37
+ 'PLANNED',
38
+ 'PICKED_UP',
39
+ 'IN_TRANSIT',
40
+ 'DELIVERED',
41
+ 'FAILED',
42
+ 'RETURNED',
43
+ ];
44
+ const EMPTY_SOURCE: ShipmentSourceDraft = {
45
+ integrationId: '',
46
+ basisTrackingEventId: '',
47
+ maxSourceAgeSeconds: '',
48
+ maxFutureSkewSeconds: '',
49
+ reason: '',
50
+ };
51
+ function label(value: string | null | undefined) {
52
+ return value ? value.toLowerCase().replaceAll('_', ' ') : 'Not recorded';
53
+ }
54
+ function time(value: string | null | undefined) {
55
+ if (!value) return 'Not recorded';
56
+ const parsed = new Date(value);
57
+ return Number.isFinite(parsed.getTime()) ? parsed.toLocaleString() : 'Unverified time';
58
+ }
59
+ function Pager({
60
+ offset,
61
+ length,
62
+ busy,
63
+ change,
64
+ }: {
65
+ offset: number;
66
+ length: number | undefined;
67
+ busy: boolean;
68
+ change: (offset: number) => void;
69
+ }) {
70
+ return (
71
+ <div className="flex flex-wrap items-center gap-3">
72
+ <Button
73
+ variant="outline"
74
+ disabled={busy || offset === 0}
75
+ onClick={() => change(Math.max(0, offset - LIMIT))}
76
+ >
77
+ Previous page
78
+ </Button>
79
+ <span>{length === undefined ? 'Page unavailable' : `${length} records on this page`}</span>
80
+ <Button
81
+ variant="outline"
82
+ disabled={busy || length !== LIMIT || offset >= 9980}
83
+ onClick={() => change(offset + LIMIT)}
84
+ >
85
+ Next page
86
+ </Button>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ export default function ShipmentStatusAuthorityPage() {
92
+ const { workspaceId, tenantId, currentUser, apiGatewayUrl } = useMoveTheWheels();
93
+ return (
94
+ <PageLayout
95
+ title="Shipment status authority"
96
+ subtitle="Keep recorded status separate from incoming tracking evidence."
97
+ icon={<ClipboardList aria-hidden="true" />}
98
+ >
99
+ <PermissionProvider scopeId={workspaceId ?? null} gateway="workspace">
100
+ <StatusWorkbench key={`${workspaceId}:${tenantId}:${currentUser?.id}:${apiGatewayUrl}`} />
101
+ </PermissionProvider>
102
+ </PageLayout>
103
+ );
104
+ }
105
+
106
+ function StatusWorkbench() {
107
+ const permissions = usePermissions();
108
+ const [offset, setOffset] = useState(0);
109
+ const [selected, setSelected] = useState('');
110
+ const [search, setSearch] = useState('');
111
+ const [filter, setFilter] = useState('');
112
+ const shipments = useShipmentStatusQuery<StatusAuthorityShipmentsQuery>(
113
+ StatusAuthorityShipmentsDocument,
114
+ permissions.hasPermission('shipment:read')
115
+ ? { filter: { search: filter || undefined }, pagination: { limit: LIMIT, offset } }
116
+ : null
117
+ );
118
+ return (
119
+ <div className="space-y-6">
120
+ <p>
121
+ Incoming evidence can be accepted while its status change is held. A recorded software
122
+ status is not physical proof of delivery.
123
+ </p>
124
+ <ReuseState loading={permissions.isLoading} error={permissions.error?.message ?? null} />
125
+ {!permissions.isLoading && !permissions.hasPermission('shipment:read') && (
126
+ <p role="status">Shipment read permission is required.</p>
127
+ )}
128
+ {permissions.hasPermission('shipment:read') && (
129
+ <section className={reuseCard} aria-label="Shipment selection">
130
+ <form
131
+ className="flex flex-wrap gap-3"
132
+ onSubmit={(event) => {
133
+ event.preventDefault();
134
+ setFilter(search.trim());
135
+ setOffset(0);
136
+ setSelected('');
137
+ }}
138
+ >
139
+ <Label htmlFor="status-search">Shipment search</Label>
140
+ <Input
141
+ id="status-search"
142
+ value={search}
143
+ onChange={(event) => setSearch(event.target.value)}
144
+ maxLength={200}
145
+ />
146
+ <Button type="submit">Search shipments</Button>
147
+ </form>
148
+ <ReuseState loading={shipments.loading} error={shipments.error} />
149
+ <label className="block">
150
+ Shipment
151
+ <select
152
+ className={reuseSelect}
153
+ value={selected}
154
+ disabled={shipments.loading || !shipments.data}
155
+ onChange={(event) => setSelected(event.target.value)}
156
+ >
157
+ <option value="">Choose a shipment</option>
158
+ {shipments.data?.shipments.map((shipment) => (
159
+ <option key={shipment.id} value={shipment.id}>
160
+ {shipment.shipmentNumber} · {label(shipment.status)}
161
+ </option>
162
+ ))}
163
+ </select>
164
+ </label>
165
+ {shipments.data?.shipments.length === 0 && <p>No shipments on this page.</p>}
166
+ <Pager
167
+ offset={offset}
168
+ length={shipments.data?.shipments.length}
169
+ busy={shipments.loading}
170
+ change={(next) => {
171
+ setOffset(next);
172
+ setSelected('');
173
+ }}
174
+ />
175
+ </section>
176
+ )}
177
+ {selected && permissions.hasPermission('shipment:read') && (
178
+ <StatusDetail
179
+ key={selected}
180
+ id={selected}
181
+ canUpdate={permissions.hasPermission('shipment:update')}
182
+ canReadSources={permissions.hasPermission('integration:read')}
183
+ />
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function StatusDetail({
190
+ id,
191
+ canUpdate,
192
+ canReadSources,
193
+ }: {
194
+ id: string;
195
+ canUpdate: boolean;
196
+ canReadSources: boolean;
197
+ }) {
198
+ const [refresh, setRefresh] = useState(0);
199
+ const [offset, setOffset] = useState(0);
200
+ const [sourceOffset, setSourceOffset] = useState(0);
201
+ const [evidenceOffset, setEvidenceOffset] = useState(0);
202
+ const action = useShipmentStatusCommand();
203
+ const detail = useShipmentStatusQuery<StatusAuthorityDetailQuery>(
204
+ StatusAuthorityDetailDocument,
205
+ {
206
+ id,
207
+ pagination: { limit: LIMIT, offset },
208
+ evidencePagination: { limit: LIMIT, offset: evidenceOffset },
209
+ },
210
+ refresh + action.revision
211
+ );
212
+ const sources = useShipmentStatusQuery<StatusAuthoritySourcesQuery>(
213
+ StatusAuthoritySourcesDocument,
214
+ canUpdate && canReadSources ? { pagination: { limit: LIMIT, offset: sourceOffset } } : null
215
+ );
216
+ const shipment = detail.data?.shipment;
217
+ // Forms stay mounted during refresh, so a stale response never erases operator input.
218
+ const [source, setSource] = useState<ShipmentSourceDraft>(EMPTY_SOURCE);
219
+ const [manualStatus, setManualStatus] = useState<ShipmentStatus | ''>('');
220
+ const [manualReason, setManualReason] = useState('');
221
+ const [formRevision, setFormRevision] = useState<number | null>(null);
222
+ const [formError, setFormError] = useState<string | null>(null);
223
+ const [reviewed, setReviewed] = useState(false);
224
+ const [requiredRefresh, setRequiredRefresh] = useState(0);
225
+ const freshReviewAvailable =
226
+ refresh >= requiredRefresh && !!shipment && !detail.loading && !detail.error;
227
+ const stale =
228
+ formRevision !== null && shipment != null && formRevision !== shipment.statusRevision;
229
+ const uncertain = action.feedback?.kind === 'UNKNOWN';
230
+ const blocked =
231
+ action.busy ||
232
+ detail.loading ||
233
+ !shipment ||
234
+ formRevision === null ||
235
+ stale ||
236
+ uncertain ||
237
+ !freshReviewAvailable;
238
+ const basis =
239
+ shipment?.trackingEvents.filter(
240
+ (event) =>
241
+ event.sourceIntegrationId === source.integrationId &&
242
+ event.sourceTimestampProvenance === 'PROVIDER_TIME_VALIDATED' &&
243
+ event.providerEventTime &&
244
+ event.eventType !== 'CREATED'
245
+ ) ?? [];
246
+ const changeSource = (field: keyof ShipmentSourceDraft, value: string) =>
247
+ setSource((current) => ({ ...current, [field]: value }));
248
+ return (
249
+ <div className="space-y-6">
250
+ <section className={reuseCard} aria-label="Recorded shipment status">
251
+ <div className="flex flex-wrap justify-between gap-3">
252
+ <h2 className="font-semibold">Recorded status</h2>
253
+ <Button
254
+ variant="outline"
255
+ disabled={detail.loading || action.busy}
256
+ onClick={() => {
257
+ setReviewed(false);
258
+ setRefresh((value) => value + 1);
259
+ }}
260
+ >
261
+ Refresh recorded status
262
+ </Button>
263
+ </div>
264
+ <ReuseState loading={detail.loading} error={detail.error} />
265
+ {shipment === null && <p>Shipment unavailable in this workspace.</p>}
266
+ {shipment && (
267
+ <dl className="grid gap-3 sm:grid-cols-2 break-words">
268
+ <div>
269
+ <dt>Shipment</dt>
270
+ <dd>{shipment.shipmentNumber}</dd>
271
+ </div>
272
+ <div>
273
+ <dt>Status</dt>
274
+ <dd>{label(shipment.status)}</dd>
275
+ </div>
276
+ <div>
277
+ <dt>Status revision</dt>
278
+ <dd>{shipment.statusRevision}</dd>
279
+ </div>
280
+ <div>
281
+ <dt>Authority</dt>
282
+ <dd>{label(shipment.statusAuthority)}</dd>
283
+ </div>
284
+ <div>
285
+ <dt>Chosen integration</dt>
286
+ <dd className="break-all">{shipment.designatedIntegrationId ?? 'None selected'}</dd>
287
+ </div>
288
+ <div>
289
+ <dt>Basis source time</dt>
290
+ <dd>{time(shipment.statusBasisSourceTime)}</dd>
291
+ </div>
292
+ <div>
293
+ <dt>Maximum source age</dt>
294
+ <dd>
295
+ {shipment.maxSourceAgeSeconds === null
296
+ ? 'No active source policy'
297
+ : `${shipment.maxSourceAgeSeconds} seconds`}
298
+ </dd>
299
+ </div>
300
+ <div>
301
+ <dt>Allowed future skew</dt>
302
+ <dd>
303
+ {shipment.maxFutureSkewSeconds === null
304
+ ? 'No active source policy'
305
+ : `${shipment.maxFutureSkewSeconds} seconds`}
306
+ </dd>
307
+ </div>
308
+ </dl>
309
+ )}
310
+ </section>
311
+ {action.feedback && <p role="status">{action.feedback.message}</p>}
312
+ {formError && <p role="alert">{formError}</p>}
313
+ {!canUpdate && (
314
+ <p role="status">
315
+ Read-only access. Shipment update permission is required to choose a source or record a
316
+ correction.
317
+ </p>
318
+ )}
319
+ {canUpdate && (
320
+ <section className={reuseCard} aria-label="Status decisions">
321
+ <p>
322
+ Review the current status, then explicitly use its revision for a new decision. Clock
323
+ limits are your policy, not carrier certification.
324
+ </p>
325
+ {stale && (
326
+ <p role="alert">
327
+ Status changed since this form opened. Your input is preserved. Review the current
328
+ recorded status before applying.
329
+ </p>
330
+ )}
331
+ {!uncertain && (
332
+ <Button
333
+ variant="outline"
334
+ disabled={!freshReviewAvailable || action.busy}
335
+ onClick={() => {
336
+ if (shipment) {
337
+ setFormRevision(shipment.statusRevision);
338
+ setFormError(null);
339
+ action.beginSeparateAction();
340
+ }
341
+ }}
342
+ >
343
+ Use reviewed revision {shipment?.statusRevision ?? '—'}
344
+ </Button>
345
+ )}
346
+ {uncertain && (
347
+ <div className="space-y-3">
348
+ <label className="flex items-start gap-2">
349
+ <input
350
+ type="checkbox"
351
+ disabled={!freshReviewAvailable}
352
+ checked={reviewed}
353
+ onChange={(event) => setReviewed(event.target.checked)}
354
+ />
355
+ I reviewed the refreshed recorded status and decision history.
356
+ </label>
357
+ <Button
358
+ variant="outline"
359
+ disabled={!reviewed || !freshReviewAvailable || action.busy}
360
+ onClick={() => {
361
+ if (shipment) {
362
+ setSource(EMPTY_SOURCE);
363
+ setManualStatus('');
364
+ setManualReason('');
365
+ setFormRevision(shipment.statusRevision);
366
+ setFormError(null);
367
+ setReviewed(false);
368
+ action.beginSeparateAction();
369
+ }
370
+ }}
371
+ >
372
+ Begin a separate new action
373
+ </Button>
374
+ </div>
375
+ )}
376
+ <form
377
+ className="space-y-3"
378
+ onSubmit={async (event) => {
379
+ event.preventDefault();
380
+ setFormError(null);
381
+ if (blocked || formRevision === null || !shipment) return;
382
+ try {
383
+ const input = shipmentSourceInput(source, formRevision);
384
+ const result = await action.run<StatusAuthorityConfigureMutation>(
385
+ StatusAuthorityConfigureDocument,
386
+ { id, input },
387
+ (data) =>
388
+ sourceStatusAcknowledged(data.configureShipmentStatusSource, {
389
+ id,
390
+ epoch: shipment.statusAuthorityEpoch,
391
+ input,
392
+ basis: basis.find((event) => event.id === input.basisTrackingEventId),
393
+ })
394
+ );
395
+ if (result) setFormRevision(null);
396
+ else {
397
+ setReviewed(false);
398
+ setRequiredRefresh(refresh + 1);
399
+ }
400
+ } catch (failure) {
401
+ setFormError(
402
+ failure instanceof Error ? failure.message : 'Review the source policy.'
403
+ );
404
+ }
405
+ }}
406
+ >
407
+ <fieldset disabled={blocked || !canReadSources} className="space-y-3">
408
+ <legend className="font-semibold">Choose a status source</legend>
409
+ {!canReadSources && (
410
+ <p>Integration read permission is required for source selection.</p>
411
+ )}
412
+ <ReuseState loading={sources.loading} error={sources.error} />
413
+ <label className="block">
414
+ Integration
415
+ <select
416
+ className={reuseSelect}
417
+ value={source.integrationId}
418
+ onChange={(event) => {
419
+ changeSource('integrationId', event.target.value);
420
+ changeSource('basisTrackingEventId', '');
421
+ }}
422
+ >
423
+ <option value="">Choose an integration</option>
424
+ {sources.data?.integrations
425
+ .filter((integration) => integration.status === 'ACTIVE')
426
+ .map((integration) => (
427
+ <option key={integration.id} value={integration.id}>
428
+ {integration.name} · {integration.provider}
429
+ </option>
430
+ ))}
431
+ </select>
432
+ </label>
433
+ <label className="block">
434
+ Recorded basis event
435
+ <select
436
+ className={reuseSelect}
437
+ value={source.basisTrackingEventId}
438
+ onChange={(event) => changeSource('basisTrackingEventId', event.target.value)}
439
+ >
440
+ <option value="">Choose an existing basis event</option>
441
+ {basis.map((event) => (
442
+ <option key={event.id} value={event.id}>
443
+ {label(event.eventType)} · {time(event.providerEventTime)} · {event.id}
444
+ </option>
445
+ ))}
446
+ </select>
447
+ </label>
448
+ <p>
449
+ This page lists evidence from the selected source with validated provider time. The
450
+ server checks its recorded receipt, source and clock window before applying.
451
+ </p>
452
+ <Label htmlFor="status-source-age">Maximum source age (seconds)</Label>
453
+ <Input
454
+ id="status-source-age"
455
+ inputMode="numeric"
456
+ value={source.maxSourceAgeSeconds}
457
+ onChange={(event) => changeSource('maxSourceAgeSeconds', event.target.value)}
458
+ />
459
+ <Label htmlFor="status-source-skew">Allowed future skew (seconds)</Label>
460
+ <Input
461
+ id="status-source-skew"
462
+ inputMode="numeric"
463
+ value={source.maxFutureSkewSeconds}
464
+ onChange={(event) => changeSource('maxFutureSkewSeconds', event.target.value)}
465
+ />
466
+ <Label htmlFor="status-source-reason">Source choice reason</Label>
467
+ <Textarea
468
+ id="status-source-reason"
469
+ maxLength={1000}
470
+ value={source.reason}
471
+ onChange={(event) => changeSource('reason', event.target.value)}
472
+ />
473
+ <Button type="submit">Apply source choice</Button>
474
+ </fieldset>
475
+ </form>
476
+ {canReadSources && (
477
+ <Pager
478
+ offset={sourceOffset}
479
+ length={sources.data?.integrations.length}
480
+ busy={sources.loading || action.busy}
481
+ change={(next) => {
482
+ setSourceOffset(next);
483
+ changeSource('integrationId', '');
484
+ changeSource('basisTrackingEventId', '');
485
+ }}
486
+ />
487
+ )}
488
+ <form
489
+ className="space-y-3"
490
+ onSubmit={async (event) => {
491
+ event.preventDefault();
492
+ setFormError(null);
493
+ if (blocked || formRevision === null || !shipment) return;
494
+ try {
495
+ if (!manualStatus) throw new Error('Choose a recorded status for this correction.');
496
+ const result = await action.run<StatusAuthorityManualMutation>(
497
+ StatusAuthorityManualDocument,
498
+ {
499
+ id,
500
+ input: {
501
+ status: manualStatus,
502
+ expectedStatusRevision: formRevision,
503
+ statusReason: statusDecisionReason(manualReason),
504
+ },
505
+ },
506
+ (data) =>
507
+ manualStatusAcknowledged(data.updateShipment, {
508
+ id,
509
+ revision: formRevision,
510
+ epoch: shipment.statusAuthorityEpoch,
511
+ status: manualStatus,
512
+ })
513
+ );
514
+ if (result) setFormRevision(null);
515
+ else {
516
+ setReviewed(false);
517
+ setRequiredRefresh(refresh + 1);
518
+ }
519
+ } catch (failure) {
520
+ setFormError(failure instanceof Error ? failure.message : 'Review the correction.');
521
+ }
522
+ }}
523
+ >
524
+ <fieldset disabled={blocked} className="space-y-3">
525
+ <legend className="font-semibold">Record a manual correction</legend>
526
+ <p>
527
+ A manual correction puts automatic tracking projection on hold. Existing evidence
528
+ remains unchanged.
529
+ </p>
530
+ <label className="block">
531
+ Recorded status
532
+ <select
533
+ className={reuseSelect}
534
+ value={manualStatus}
535
+ onChange={(event) => {
536
+ const selectedStatus = STATUSES.find((status) => status === event.target.value);
537
+ setManualStatus(selectedStatus ?? '');
538
+ }}
539
+ >
540
+ <option value="">Choose a status</option>
541
+ {STATUSES.map((status) => (
542
+ <option key={status} value={status}>
543
+ {label(status)}
544
+ </option>
545
+ ))}
546
+ </select>
547
+ </label>
548
+ <Label htmlFor="status-manual-reason">Correction reason</Label>
549
+ <Textarea
550
+ id="status-manual-reason"
551
+ value={manualReason}
552
+ maxLength={1000}
553
+ onChange={(event) => setManualReason(event.target.value)}
554
+ />
555
+ <Button type="submit">Record correction and hold</Button>
556
+ </fieldset>
557
+ </form>
558
+ </section>
559
+ )}
560
+ <section className={reuseCard} aria-label="Shipment status decision history">
561
+ <h2 className="font-semibold">Decision history</h2>
562
+ {detail.data?.shipmentStatusDecisions.length === 0 && (
563
+ <p>
564
+ No recorded status decisions on this page. Legacy evidence may have no decision ledger.
565
+ </p>
566
+ )}
567
+ <div className="space-y-3">
568
+ {detail.data?.shipmentStatusDecisions.map((decision) => (
569
+ <article key={decision.id} className="border rounded-md p-3 break-words">
570
+ <p>
571
+ {label(decision.origin)} · {label(decision.outcome)}
572
+ {decision.heldReason ? `: ${label(decision.heldReason)}` : ''}
573
+ </p>
574
+ <p>
575
+ {label(decision.priorStatus)} → {label(decision.resultStatus)} · revision{' '}
576
+ {decision.observedRevision} → {decision.resultRevision}
577
+ </p>
578
+ <p>{time(decision.recordedAt)}</p>
579
+ {decision.reason && <p>{decision.reason}</p>}
580
+ {decision.trackingEventId && (
581
+ <p className="break-all">Evidence: {decision.trackingEventId}</p>
582
+ )}
583
+ {decision.proofId && <p className="break-all">Delivery proof: {decision.proofId}</p>}
584
+ {decision.configuredIntegrationId && (
585
+ <p className="break-all">
586
+ Chosen integration for this decision: {decision.configuredIntegrationId}
587
+ </p>
588
+ )}
589
+ {decision.configuredMaxSourceAgeSeconds !== null && (
590
+ <p>
591
+ Source age limit for this decision: {decision.configuredMaxSourceAgeSeconds}{' '}
592
+ seconds
593
+ </p>
594
+ )}
595
+ {decision.configuredMaxFutureSkewSeconds !== null && (
596
+ <p>
597
+ Future skew limit for this decision: {decision.configuredMaxFutureSkewSeconds}{' '}
598
+ seconds
599
+ </p>
600
+ )}
601
+ </article>
602
+ ))}
603
+ </div>
604
+ <Pager
605
+ offset={offset}
606
+ length={detail.data?.shipmentStatusDecisions.length}
607
+ busy={detail.loading || action.busy}
608
+ change={(next) => {
609
+ setOffset(next);
610
+ }}
611
+ />
612
+ </section>
613
+ <section className={reuseCard} aria-label="Recorded tracking evidence">
614
+ <h2 className="font-semibold">Tracking evidence</h2>
615
+ <div className="space-y-3">
616
+ {shipment?.trackingEvents.map((event) => (
617
+ <article key={event.id} className="border rounded-md p-3 break-words">
618
+ <p>
619
+ {label(event.eventType)} · {label(event.sourceTimestampProvenance)}
620
+ </p>
621
+ <p>Provider time: {time(event.providerEventTime)}</p>
622
+ <p>Received: {time(event.serverReceivedAt)}</p>
623
+ <p className="break-all">Evidence: {event.id}</p>
624
+ </article>
625
+ ))}
626
+ </div>
627
+ <Pager
628
+ offset={evidenceOffset}
629
+ length={shipment?.trackingEvents.length}
630
+ busy={detail.loading || action.busy}
631
+ change={(next) => {
632
+ setEvidenceOffset(next);
633
+ changeSource('basisTrackingEventId', '');
634
+ }}
635
+ />
636
+ </section>
637
+ </div>
638
+ );
639
+ }
@@ -168,6 +168,16 @@ function QualityWorkbench() {
168
168
  {outcome.errorCode && (
169
169
  <p>Reason: {outcome.errorCode.replaceAll('_', ' ').toLowerCase()}</p>
170
170
  )}
171
+ {(outcome.status === 'ACCEPTED' || outcome.status === 'DUPLICATE') && (
172
+ <p>
173
+ Status decision:{' '}
174
+ {outcome.projection?.status === 'APPLIED'
175
+ ? 'Applied to the recorded shipment status'
176
+ : outcome.projection?.status === 'HELD'
177
+ ? `Held — ${outcome.projection.heldReason?.replaceAll('_', ' ').toLowerCase()}`
178
+ : 'No status decision recorded in this legacy result'}
179
+ </p>
180
+ )}
171
181
  {outcome.status === 'CONFLICT' && (
172
182
  <p>The earlier receipt stays unchanged; this update adds no event.</p>
173
183
  )}
@@ -198,8 +208,9 @@ function QualityWorkbench() {
198
208
  <QualityShipment key={shipmentId} id={shipmentId} onClose={() => setShipmentId(null)} />
199
209
  )}
200
210
  <p className="text-sm text-text-secondary">
201
- These results describe recorded ingestion, not physical shipment truth. Ordering, late
202
- updates and provider corrections need separate policies.
211
+ These results describe recorded ingestion, not physical shipment truth. The chosen-source
212
+ clock policy and manual corrections control recorded shipment status. Provider corrections
213
+ and replay need separate reviewed policies.
203
214
  </p>
204
215
  </div>
205
216
  );