@striae-org/striae 4.3.4 → 5.0.0

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 (42) hide show
  1. package/.env.example +4 -0
  2. package/app/components/actions/case-export/download-handlers.ts +60 -4
  3. package/app/components/actions/case-import/confirmation-import.ts +50 -7
  4. package/app/components/actions/case-import/confirmation-package.ts +99 -22
  5. package/app/components/actions/case-import/orchestrator.ts +116 -13
  6. package/app/components/actions/case-import/validation.ts +171 -7
  7. package/app/components/actions/case-import/zip-processing.ts +224 -127
  8. package/app/components/actions/case-manage.ts +64 -4
  9. package/app/components/actions/confirm-export.ts +32 -3
  10. package/app/components/navbar/navbar.module.css +0 -10
  11. package/app/components/navbar/navbar.tsx +0 -22
  12. package/app/components/sidebar/case-import/case-import.module.css +7 -131
  13. package/app/components/sidebar/case-import/case-import.tsx +7 -14
  14. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
  15. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
  16. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
  17. package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
  18. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
  19. package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
  20. package/app/config-example/config.json +5 -0
  21. package/app/routes/auth/login.tsx +1 -1
  22. package/app/utils/data/operations/signing-operations.ts +93 -0
  23. package/app/utils/data/operations/types.ts +6 -0
  24. package/app/utils/forensics/export-encryption.ts +316 -0
  25. package/app/utils/forensics/export-verification.ts +1 -409
  26. package/app/utils/forensics/index.ts +1 -0
  27. package/app/utils/ui/case-messages.ts +5 -2
  28. package/package.json +1 -1
  29. package/scripts/deploy-config.sh +97 -3
  30. package/scripts/deploy-worker-secrets.sh +1 -1
  31. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  32. package/workers/data-worker/src/data-worker.example.ts +130 -0
  33. package/workers/data-worker/src/encryption-utils.ts +125 -0
  34. package/workers/data-worker/worker-configuration.d.ts +1 -1
  35. package/workers/data-worker/wrangler.jsonc.example +2 -2
  36. package/workers/image-worker/wrangler.jsonc.example +1 -1
  37. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  38. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  39. package/workers/user-worker/wrangler.jsonc.example +1 -1
  40. package/wrangler.toml.example +1 -1
  41. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
  42. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
@@ -22,7 +22,9 @@ import { getImageUrl } from './image-manage';
22
22
  import {
23
23
  calculateSHA256Secure,
24
24
  createPublicSigningKeyFileName,
25
+ encryptExportDataWithAllImages,
25
26
  generateForensicManifestSecure,
27
+ getCurrentEncryptionPublicKeyDetails,
26
28
  getCurrentPublicSigningKeyDetails,
27
29
  getVerificationPublicKey,
28
30
  } from '~/utils/forensics';
@@ -897,8 +899,64 @@ export const archiveCase = async (
897
899
  auditTrail,
898
900
  };
899
901
 
900
- zip.file('audit/case-audit-trail.json', JSON.stringify(signedAuditTrail, null, 2));
901
- zip.file('audit/case-audit-signature.json', JSON.stringify(signedAuditExportPayload, null, 2));
902
+ const auditTrailJson = JSON.stringify(signedAuditTrail, null, 2);
903
+ const auditSignatureJson = JSON.stringify(signedAuditExportPayload, null, 2);
904
+ zip.file('audit/case-audit-trail.json', auditTrailJson);
905
+ zip.file('audit/case-audit-signature.json', auditSignatureJson);
906
+
907
+ const encryptionKeyDetails = getCurrentEncryptionPublicKeyDetails();
908
+
909
+ if (!encryptionKeyDetails.publicKeyPem || !encryptionKeyDetails.keyId) {
910
+ throw new Error(
911
+ 'Archive encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
912
+ 'Please contact your administrator to set up export encryption.'
913
+ );
914
+ }
915
+
916
+ try {
917
+ const filesToEncrypt: Array<{ filename: string; blob: Blob }> = [
918
+ ...Object.entries(imageBlobs).map(([filename, blob]) => ({
919
+ filename,
920
+ blob
921
+ })),
922
+ {
923
+ filename: 'audit/case-audit-trail.json',
924
+ blob: new Blob([auditTrailJson], { type: 'application/json' })
925
+ },
926
+ {
927
+ filename: 'audit/case-audit-signature.json',
928
+ blob: new Blob([auditSignatureJson], { type: 'application/json' })
929
+ }
930
+ ];
931
+
932
+ const encryptionResult = await encryptExportDataWithAllImages(
933
+ caseJsonContent,
934
+ filesToEncrypt,
935
+ encryptionKeyDetails.publicKeyPem,
936
+ encryptionKeyDetails.keyId
937
+ );
938
+
939
+ zip.file(`${caseNumber}_data.json`, encryptionResult.ciphertext);
940
+
941
+ for (let index = 0; index < filesToEncrypt.length; index += 1) {
942
+ const originalFilename = filesToEncrypt[index].filename;
943
+ const encryptedContent = encryptionResult.encryptedImages[index];
944
+
945
+ if (originalFilename.startsWith('audit/')) {
946
+ zip.file(originalFilename, encryptedContent);
947
+ continue;
948
+ }
949
+
950
+ if (imageFolder) {
951
+ imageFolder.file(originalFilename, encryptedContent);
952
+ }
953
+ }
954
+
955
+ zip.file('ENCRYPTION_MANIFEST.json', JSON.stringify(encryptionResult.encryptionManifest, null, 2));
956
+ } catch (error) {
957
+ console.error('Archive encryption failed:', error);
958
+ throw new Error(`Failed to encrypt archive package: ${error instanceof Error ? error.message : 'Unknown error'}`);
959
+ }
902
960
 
903
961
  zip.file(
904
962
  'README.txt',
@@ -913,12 +971,14 @@ export const archiveCase = async (
913
971
  '',
914
972
  'Package Contents',
915
973
  '- Case data JSON export with all image references',
916
- '- images/ folder with exported image files',
974
+ '- images/ folder with exported image files (encrypted)',
917
975
  '- Full case audit trail export and signed audit metadata',
918
976
  '- Forensic manifest with server-side signature',
977
+ '- ENCRYPTION_MANIFEST.json with encryption metadata and encrypted image hashes',
919
978
  `- ${publicKeyFileName} for verification`,
920
979
  '',
921
980
  'This package is intended for read-only review and verification workflows.',
981
+ 'This package is encrypted. Only Striae can decrypt and re-import it.',
922
982
  ].join('\n')
923
983
  );
924
984
 
@@ -950,7 +1010,7 @@ export const archiveCase = async (
950
1010
  );
951
1011
 
952
1012
  const downloadUrl = URL.createObjectURL(zipBlob);
953
- const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}.zip`;
1013
+ const archiveFileName = `striae-case-${caseNumber}-archive-${formatDateForFilename(new Date())}-encrypted.zip`;
954
1014
  const anchor = document.createElement('a');
955
1015
  anchor.href = downloadUrl;
956
1016
  anchor.download = archiveFileName;
@@ -3,7 +3,9 @@ import {
3
3
  calculateSHA256Secure,
4
4
  createPublicSigningKeyFileName,
5
5
  getCurrentPublicSigningKeyDetails,
6
- getVerificationPublicKey
6
+ getVerificationPublicKey,
7
+ getCurrentEncryptionPublicKeyDetails,
8
+ encryptExportDataWithAllImages
7
9
  } from '~/utils/forensics';
8
10
  import { getUserData, getCaseData, updateCaseData, signConfirmationData, upsertFileConfirmationSummary } from '~/utils/data';
9
11
  import { type AnnotationData, type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
@@ -318,8 +320,35 @@ export async function exportConfirmationData(
318
320
  const zip = new JSZip();
319
321
  const normalizedPem = publicKeyPem.endsWith('\n') ? publicKeyPem : `${publicKeyPem}\n`;
320
322
 
321
- zip.file(confirmationFileName, finalJsonString);
323
+ const encKeyDetails = getCurrentEncryptionPublicKeyDetails();
324
+ if (!encKeyDetails.publicKeyPem || !encKeyDetails.keyId) {
325
+ throw new Error(
326
+ 'Confirmation export encryption is mandatory. Your Striae instance does not have a configured encryption public key. ' +
327
+ 'Please contact your administrator to set up export encryption.'
328
+ );
329
+ }
330
+
331
+ let encryptedConfirmationContent: string | Uint8Array;
332
+ let encryptionManifestJson: string;
333
+
334
+ try {
335
+ const encryptionResult = await encryptExportDataWithAllImages(
336
+ finalJsonString,
337
+ [],
338
+ encKeyDetails.publicKeyPem,
339
+ encKeyDetails.keyId
340
+ );
341
+
342
+ encryptedConfirmationContent = encryptionResult.ciphertext;
343
+ encryptionManifestJson = JSON.stringify(encryptionResult.encryptionManifest, null, 2);
344
+ } catch (error) {
345
+ console.error('Confirmation export encryption failed:', error);
346
+ throw new Error(`Failed to encrypt confirmation export: ${error instanceof Error ? error.message : 'Unknown error'}`);
347
+ }
348
+
349
+ zip.file(confirmationFileName, encryptedConfirmationContent);
322
350
  zip.file(publicKeyFileName, normalizedPem);
351
+ zip.file('ENCRYPTION_MANIFEST.json', encryptionManifestJson);
323
352
 
324
353
  const zipBlob = await zip.generateAsync({
325
354
  type: 'blob',
@@ -327,7 +356,7 @@ export async function exportConfirmationData(
327
356
  compressionOptions: { level: 6 }
328
357
  });
329
358
 
330
- const exportFileName = `confirmation-export-${caseNumber}-${timestampString}.zip`;
359
+ const exportFileName = `confirmation-export-${caseNumber}-${timestampString}-encrypted.zip`;
331
360
 
332
361
  // Create download
333
362
  const url = URL.createObjectURL(zipBlob);
@@ -374,16 +374,6 @@
374
374
  background: color-mix(in lab, #6c757d 21%, #ffffff);
375
375
  }
376
376
 
377
- .caseMenuItemKey {
378
- background: color-mix(in lab, #0d9488 14%, #ffffff);
379
- color: #0f766e;
380
- border-color: color-mix(in lab, #0d9488 28%, transparent);
381
- }
382
-
383
- .caseMenuItemKey:hover {
384
- background: color-mix(in lab, #0d9488 20%, #ffffff);
385
- }
386
-
387
377
  .caseMenuItemClearRO {
388
378
  background: color-mix(in lab, #fd7e14 16%, #ffffff);
389
379
  color: #7c3f00;
@@ -3,8 +3,6 @@ import styles from './navbar.module.css';
3
3
  import { SignOut } from '../actions/signout';
4
4
  import { ManageProfile } from '../user/manage-profile';
5
5
  import { CaseImport } from '../sidebar/case-import/case-import';
6
- import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
7
- import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
8
6
  import { AuthContext } from '~/contexts/auth.context';
9
7
  import { getUserData } from '~/utils/data';
10
8
  import { type ImportResult, type ConfirmationImportResult } from '~/types';
@@ -68,10 +66,8 @@ export const Navbar = ({
68
66
  const [userBadgeId, setUserBadgeId] = useState<string>('');
69
67
  const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
70
68
  const [isImportModalOpen, setIsImportModalOpen] = useState(false);
71
- const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
72
69
  const [isCaseMenuOpen, setIsCaseMenuOpen] = useState(false);
73
70
  const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
74
- const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
75
71
  const caseMenuRef = useRef<HTMLDivElement>(null);
76
72
  const fileMenuRef = useRef<HTMLDivElement>(null);
77
73
 
@@ -292,18 +288,6 @@ export const Navbar = ({
292
288
  Archive Case
293
289
  </button>
294
290
  )}
295
- <div className={styles.caseMenuSectionLabel}>Verification</div>
296
- <button
297
- type="button"
298
- role="menuitem"
299
- className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
300
- onClick={() => {
301
- setIsPublicKeyModalOpen(true);
302
- setIsCaseMenuOpen(false);
303
- }}
304
- >
305
- Verify Exports
306
- </button>
307
291
  {currentCase && (
308
292
  <div className={styles.caseMenuCaption}>Case: {currentCase}</div>
309
293
  )}
@@ -423,12 +407,6 @@ export const Navbar = ({
423
407
  isOpen={isProfileModalOpen}
424
408
  onClose={() => setIsProfileModalOpen(false)}
425
409
  />
426
- <PublicSigningKeyModal
427
- isOpen={isPublicKeyModalOpen}
428
- onClose={() => setIsPublicKeyModalOpen(false)}
429
- publicSigningKeyId={publicSigningKeyId}
430
- publicKeyPem={publicKeyPem}
431
- />
432
410
  </>
433
411
  );
434
412
  };
@@ -425,6 +425,13 @@
425
425
  color: var(--textTitle);
426
426
  }
427
427
 
428
+ .previewMessage {
429
+ margin: 0;
430
+ color: var(--textBody);
431
+ font-size: var(--fontSizeBodyS);
432
+ line-height: 1.5;
433
+ }
434
+
428
435
  .archivedImportNote {
429
436
  margin-bottom: var(--spaceM);
430
437
  padding: var(--spaceS) var(--spaceM);
@@ -447,62 +454,6 @@
447
454
  font-weight: var(--fontWeightMedium);
448
455
  }
449
456
 
450
- /* Validation Section - Green/Red Based on Status */
451
- .validationSection {
452
- border-radius: var(--spaceXS);
453
- padding: var(--spaceM);
454
- margin: var(--spaceM) 0;
455
- background: color-mix(in lab, var(--primary) 5%, transparent);
456
- border: 1px solid color-mix(in lab, var(--primary) 15%, transparent);
457
- }
458
-
459
- .validationSectionValid {
460
- background: color-mix(in lab, var(--success) 8%, transparent) !important;
461
- border: 2px solid color-mix(in lab, var(--success) 25%, transparent) !important;
462
- box-shadow: 0 2px 6px color-mix(in lab, var(--success) 12%, transparent) !important;
463
- }
464
-
465
- .validationSectionInvalid {
466
- background: color-mix(in lab, var(--error) 8%, transparent) !important;
467
- border: 2px solid color-mix(in lab, var(--error) 25%, transparent) !important;
468
- box-shadow: 0 2px 6px color-mix(in lab, var(--error) 12%, transparent) !important;
469
- }
470
-
471
- .validationTitle {
472
- margin: 0 0 var(--spaceM) 0;
473
- font-size: var(--fontSizeBodyM);
474
- font-weight: var(--fontWeightMedium);
475
- color: var(--textTitle);
476
- }
477
-
478
- .validationItem {
479
- display: flex;
480
- justify-content: space-between;
481
- align-items: center;
482
- padding: var(--spaceXS) 0;
483
- font-size: var(--fontSizeBodyS);
484
- }
485
-
486
- .validationLabel {
487
- font-weight: var(--fontWeightMedium);
488
- color: var(--textTitle);
489
- }
490
-
491
- .validationValue {
492
- font-family: var(--fontMono);
493
- font-size: var(--fontSizeBodyXS);
494
- }
495
-
496
- .validationSuccess {
497
- color: var(--success);
498
- font-weight: var(--fontWeightMedium);
499
- }
500
-
501
- .validationError {
502
- color: var(--error);
503
- font-weight: var(--fontWeightMedium);
504
- }
505
-
506
457
  .previewLoading {
507
458
  text-align: center;
508
459
  color: var(--textLight);
@@ -510,31 +461,6 @@
510
461
  padding: var(--spaceM);
511
462
  }
512
463
 
513
- .previewGrid {
514
- display: grid;
515
- gap: var(--spaceS);
516
- }
517
-
518
- .previewItem {
519
- display: flex;
520
- justify-content: space-between;
521
- align-items: center;
522
- padding: var(--spaceXS) 0;
523
- }
524
-
525
- .previewLabel {
526
- font-weight: var(--fontWeightMedium);
527
- color: var(--textBody);
528
- font-size: var(--fontSizeBodyS);
529
- }
530
-
531
- .previewValue {
532
- color: var(--textTitle);
533
- font-size: var(--fontSizeBodyS);
534
- text-align: right;
535
- font-weight: var(--fontWeightMedium);
536
- }
537
-
538
464
  /* Confirmation Dialog */
539
465
  .confirmationOverlay {
540
466
  position: fixed;
@@ -582,45 +508,6 @@
582
508
  margin: 0 0 var(--spaceL) 0;
583
509
  }
584
510
 
585
- .confirmationItem {
586
- display: flex;
587
- justify-content: space-between;
588
- align-items: center;
589
- padding: var(--spaceXS) 0;
590
- font-size: var(--fontSizeBodyS);
591
- }
592
-
593
- .confirmationItem:not(:last-child) {
594
- border-bottom: 1px solid color-mix(in lab, var(--text) 5%, transparent);
595
- }
596
-
597
- .confirmationItemValid {
598
- background: color-mix(in lab, var(--success) 15%, transparent);
599
- border-radius: var(--spaceXS);
600
- padding: var(--spaceS) var(--spaceM);
601
- margin: var(--spaceXS) calc(-1 * var(--spaceM));
602
- border: 2px solid color-mix(in lab, var(--success) 35%, transparent);
603
- box-shadow: 0 2px 4px color-mix(in lab, var(--success) 10%, transparent);
604
- }
605
-
606
- .confirmationItemInvalid {
607
- background: color-mix(in lab, var(--error) 8%, transparent);
608
- border-radius: var(--spaceXS);
609
- padding: var(--spaceS) var(--spaceM);
610
- margin: var(--spaceXS) calc(-1 * var(--spaceM));
611
- border: 1px solid color-mix(in lab, var(--error) 20%, transparent);
612
- }
613
-
614
- .confirmationSuccess {
615
- color: var(--success);
616
- font-weight: var(--fontWeightMedium);
617
- }
618
-
619
- .confirmationError {
620
- color: var(--error);
621
- font-weight: var(--fontWeightMedium);
622
- }
623
-
624
511
  .confirmationButtons {
625
512
  display: flex;
626
513
  gap: var(--spaceM);
@@ -642,14 +529,3 @@
642
529
  .confirmButton:hover {
643
530
  box-shadow: 0 2px 6px color-mix(in lab, var(--primary) 30%, transparent);
644
531
  }
645
-
646
- .hashWarning {
647
- background: color-mix(in lab, var(--error) 10%, transparent);
648
- border: 1px solid color-mix(in lab, var(--error) 30%, transparent);
649
- color: var(--error);
650
- padding: var(--spaceM);
651
- border-radius: var(--spaceXS);
652
- margin-top: var(--spaceM);
653
- font-size: var(--fontSizeBodyS);
654
- font-weight: var(--fontWeightMedium);
655
- }
@@ -4,8 +4,7 @@ import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
4
4
  import {
5
5
  ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
6
6
  IMPORT_FILE_TYPE_NOT_ALLOWED,
7
- IMPORT_FILE_TYPE_NOT_SUPPORTED,
8
- DATA_INTEGRITY_BLOCKED_TAMPERING
7
+ IMPORT_FILE_TYPE_NOT_SUPPORTED
9
8
  } from '~/utils/ui';
10
9
  import {
11
10
  listReadOnlyCases,
@@ -177,14 +176,14 @@ export const CaseImport = ({
177
176
  clearMessages();
178
177
 
179
178
  if (!isValidImportFile(file)) {
180
- setError('Only Striae case ZIP files, confirmation ZIP files, or confirmation JSON files are allowed.');
179
+ setError(IMPORT_FILE_TYPE_NOT_ALLOWED);
181
180
  clearImportData();
182
181
  return;
183
182
  }
184
183
 
185
184
  const importType = await resolveImportType(file);
186
185
  if (!importType) {
187
- setError('The selected file is not a supported Striae case or confirmation import package.');
186
+ setError(IMPORT_FILE_TYPE_NOT_SUPPORTED);
188
187
  clearImportData();
189
188
  return;
190
189
  }
@@ -415,13 +414,6 @@ export const CaseImport = ({
415
414
  {/* Import progress */}
416
415
  <ProgressSection importProgress={importProgress} />
417
416
 
418
- {/* Hash validation warning */}
419
- {casePreview?.hashValid === false && (
420
- <div className={styles.hashWarning}>
421
- <strong>⚠️ Import Blocked:</strong> {DATA_INTEGRITY_BLOCKED_TAMPERING}
422
- </div>
423
- )}
424
-
425
417
  {isArchivedRegularCaseImportBlocked && (
426
418
  <div className={styles.error}>{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}</div>
427
419
  )}
@@ -452,7 +444,7 @@ export const CaseImport = ({
452
444
  importState.isClearing ||
453
445
  importState.isLoadingPreview ||
454
446
  (importState.importType === 'case' && isArchivedRegularCaseImportBlocked) ||
455
- (importState.importType === 'case' && (!casePreview || casePreview.hashValid !== true))
447
+ (importState.importType === 'case' && (!casePreview || casePreview.hashValid === false))
456
448
  }
457
449
  >
458
450
  {importState.isImporting ? 'Importing...' :
@@ -472,15 +464,16 @@ export const CaseImport = ({
472
464
  <div className={styles.instructions}>
473
465
  <h3 className={styles.instructionsTitle}>Case Review Instructions:</h3>
474
466
  <ul className={styles.instructionsList}>
475
- <li>Only ZIP files (.zip) exported with the JSON data format from Striae are accepted</li>
467
+ <li>Only case ZIP packages exported from Striae are accepted</li>
476
468
  <li>Only one case can be reviewed at a time</li>
477
469
  <li>Imported cases are read-only and cannot be modified</li>
470
+ <li>Integrity and signature validation are enforced during import</li>
478
471
  <li>Importing will automatically replace any existing review case</li>
479
472
  </ul>
480
473
  <br />
481
474
  <h3 className={styles.instructionsTitle}>Confirmation Import Instructions:</h3>
482
475
  <ul className={styles.instructionsList}>
483
- <li>Confirmation imports accept either confirmation JSON files or confirmation ZIP packages exported from Striae</li>
476
+ <li>Confirmation imports accept only encrypted confirmation ZIP packages exported from Striae</li>
484
477
  <li>Only one confirmation file can be imported at a time</li>
485
478
  <li>Confirmed images will become read-only and cannot be modified</li>
486
479
  <li>If an image has a pre-existing confirmation, it will be skipped</li>
@@ -1,5 +1,8 @@
1
1
  import { type CaseImportPreview } from '~/types';
2
- import { ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE, DATA_INTEGRITY_VALIDATION_PASSED, DATA_INTEGRITY_VALIDATION_FAILED } from '~/utils/ui';
2
+ import {
3
+ ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
4
+ ARCHIVED_SELF_IMPORT_NOTE
5
+ } from '~/utils/ui';
3
6
  import styles from '../case-import.module.css';
4
7
 
5
8
  interface CasePreviewSectionProps {
@@ -26,67 +29,21 @@ export const CasePreviewSection = ({
26
29
  if (!casePreview) return null;
27
30
 
28
31
  return (
29
- <>
30
- {/* Case Information - Always Blue */}
31
- <div className={styles.previewSection}>
32
- <h3 className={styles.previewTitle}>Case Information</h3>
33
- {casePreview.archived && (
34
- <div className={styles.archivedImportNote}>
35
- Archived export detected. Original exporter imports are allowed for archived cases.
36
- </div>
37
- )}
38
- {isArchivedRegularCaseImportBlocked && (
39
- <div className={styles.archivedRegularCaseRiskNote}>
40
- {ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}
41
- </div>
42
- )}
43
- <div className={styles.previewGrid}>
44
- <div className={styles.previewItem}>
45
- <span className={styles.previewLabel}>Case Number:</span>
46
- <span className={styles.previewValue}>{casePreview.caseNumber}</span>
47
- </div>
48
- <div className={styles.previewItem}>
49
- <span className={styles.previewLabel}>Exported by:</span>
50
- <span className={styles.previewValue}>
51
- {casePreview.exportedByName || casePreview.exportedBy || 'N/A'}
52
- </span>
53
- </div>
54
- <div className={styles.previewItem}>
55
- <span className={styles.previewLabel}>Lab/Company:</span>
56
- <span className={styles.previewValue}>{casePreview.exportedByCompany || 'N/A'}</span>
57
- </div>
58
- <div className={styles.previewItem}>
59
- <span className={styles.previewLabel}>Export Date:</span>
60
- <span className={styles.previewValue}>
61
- {new Date(casePreview.exportDate).toLocaleDateString()}
62
- </span>
63
- </div>
64
- <div className={styles.previewItem}>
65
- <span className={styles.previewLabel}>Total Images:</span>
66
- <span className={styles.previewValue}>{casePreview.totalFiles}</span>
67
- </div>
68
- <div className={styles.previewItem}>
69
- <span className={styles.previewLabel}>Archived Export:</span>
70
- <span className={styles.previewValue}>{casePreview.archived ? 'Yes' : 'No'}</span>
71
- </div>
32
+ <div className={styles.previewSection}>
33
+ <h3 className={styles.previewTitle}>Case Import Preview</h3>
34
+ <p className={styles.previewMessage}>
35
+ Case package detected. Details are hidden until import verification completes.
36
+ </p>
37
+ {casePreview.archived && (
38
+ <div className={styles.archivedImportNote}>
39
+ {ARCHIVED_SELF_IMPORT_NOTE}
72
40
  </div>
73
- </div>
74
-
75
- {/* Data Integrity Checks - Green/Red Based on Validation */}
76
- {casePreview.hashValid !== undefined && (
77
- <div className={`${styles.validationSection} ${casePreview.hashValid ? styles.validationSectionValid : styles.validationSectionInvalid}`}>
78
- <h3 className={styles.validationTitle}>Data Integrity Validation</h3>
79
- <div className={styles.validationItem}>
80
- <span className={`${styles.validationValue} ${casePreview.hashValid ? styles.validationSuccess : styles.validationError}`}>
81
- {casePreview.hashValid ? (
82
- <>{DATA_INTEGRITY_VALIDATION_PASSED}</>
83
- ) : (
84
- <>{DATA_INTEGRITY_VALIDATION_FAILED}</>
85
- )}
86
- </span>
87
- </div>
41
+ )}
42
+ {isArchivedRegularCaseImportBlocked && (
43
+ <div className={styles.archivedRegularCaseRiskNote}>
44
+ {ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}
88
45
  </div>
89
46
  )}
90
- </>
47
+ </div>
91
48
  );
92
49
  };
@@ -1,5 +1,8 @@
1
1
  import { type CaseImportPreview } from '~/types';
2
- import { ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE, DATA_INTEGRITY_VALIDATION_PASSED, DATA_INTEGRITY_VALIDATION_FAILED } from '~/utils/ui';
2
+ import {
3
+ ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE,
4
+ ARCHIVED_SELF_IMPORT_NOTE
5
+ } from '~/utils/ui';
3
6
  import styles from '../case-import.module.css';
4
7
 
5
8
  interface ConfirmationDialogProps {
@@ -21,6 +24,8 @@ export const ConfirmationDialog = ({
21
24
  }: ConfirmationDialogProps) => {
22
25
  if (!showConfirmation || !casePreview) return null;
23
26
 
27
+ const hasDetails = casePreview.archived || isArchivedRegularCaseImportBlocked;
28
+
24
29
  return (
25
30
  <div className={styles.confirmationOverlay}>
26
31
  <div className={styles.confirmationModal}>
@@ -29,45 +34,24 @@ export const ConfirmationDialog = ({
29
34
  <p className={styles.confirmationText}>
30
35
  Are you sure you want to import this case for review?
31
36
  </p>
32
-
33
- <div className={styles.confirmationDetails}>
34
- <div className={styles.confirmationItem}>
35
- <strong>Case Number:</strong> {casePreview.caseNumber}
36
- </div>
37
- <div className={styles.confirmationItem}>
38
- <strong>Exported by:</strong> {casePreview.exportedByName || casePreview.exportedBy || 'N/A'}
39
- </div>
40
- <div className={styles.confirmationItem}>
41
- <strong>Lab/Company:</strong> {casePreview.exportedByCompany || 'N/A'}
42
- </div>
43
- <div className={styles.confirmationItem}>
44
- <strong>Export Date:</strong> {new Date(casePreview.exportDate).toLocaleDateString()}
45
- </div>
46
- <div className={styles.confirmationItem}>
47
- <strong>Total Images:</strong> {casePreview.totalFiles}
48
- </div>
49
- <div className={styles.confirmationItem}>
50
- <strong>Archived Export:</strong> {casePreview.archived ? 'Yes' : 'No'}
37
+ <p className={styles.confirmationText}>
38
+ Package details stay hidden until verification completes.
39
+ </p>
40
+
41
+ {hasDetails && (
42
+ <div className={styles.confirmationDetails}>
43
+ {casePreview.archived && (
44
+ <div className={styles.archivedImportNote}>
45
+ {ARCHIVED_SELF_IMPORT_NOTE}
46
+ </div>
47
+ )}
48
+ {isArchivedRegularCaseImportBlocked && (
49
+ <div className={styles.archivedRegularCaseRiskNote}>
50
+ {archivedRegularCaseBlockMessage}
51
+ </div>
52
+ )}
51
53
  </div>
52
- {casePreview.archived && (
53
- <div className={styles.archivedImportNote}>
54
- Archived export detected. Original exporter imports are allowed for archived cases.
55
- </div>
56
- )}
57
- {isArchivedRegularCaseImportBlocked && (
58
- <div className={styles.archivedRegularCaseRiskNote}>
59
- {archivedRegularCaseBlockMessage}
60
- </div>
61
- )}
62
- {casePreview.hashValid !== undefined && (
63
- <div className={`${styles.confirmationItem} ${casePreview.hashValid ? styles.confirmationItemValid : styles.confirmationItemInvalid}`}>
64
- <strong>Data Integrity:</strong>
65
- <span className={casePreview.hashValid ? styles.confirmationSuccess : styles.confirmationError}>
66
- {casePreview.hashValid ? DATA_INTEGRITY_VALIDATION_PASSED : DATA_INTEGRITY_VALIDATION_FAILED}
67
- </span>
68
- </div>
69
- )}
70
- </div>
54
+ )}
71
55
 
72
56
  <div className={styles.confirmationButtons}>
73
57
  <button