@striae-org/striae 4.1.0 → 4.2.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 (91) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +9 -2
  12. package/app/components/actions/image-manage.ts +77 -44
  13. package/app/components/audit/user-audit-viewer.tsx +19 -8
  14. package/app/components/audit/user-audit.module.css +21 -0
  15. package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
  16. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  17. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  18. package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
  19. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  20. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  21. package/app/components/canvas/canvas.module.css +64 -54
  22. package/app/components/canvas/canvas.tsx +14 -16
  23. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  24. package/app/components/canvas/confirmation/confirmation.tsx +6 -12
  25. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  26. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  27. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  28. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  29. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  30. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  31. package/app/components/navbar/navbar.module.css +447 -0
  32. package/app/components/navbar/navbar.tsx +377 -0
  33. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  34. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  35. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  36. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  37. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  38. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  39. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  40. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  41. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  42. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  43. package/app/components/sidebar/cases/cases-modal.tsx +6 -8
  44. package/app/components/sidebar/cases/cases.module.css +62 -21
  45. package/app/components/sidebar/files/files-modal.module.css +1 -0
  46. package/app/components/sidebar/files/files-modal.tsx +12 -13
  47. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  48. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  49. package/app/components/sidebar/notes/notes-modal.tsx +7 -8
  50. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  51. package/app/components/sidebar/notes/notes.module.css +153 -0
  52. package/app/components/sidebar/sidebar-container.tsx +15 -28
  53. package/app/components/sidebar/sidebar.module.css +5 -69
  54. package/app/components/sidebar/sidebar.tsx +24 -125
  55. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  56. package/app/components/user/inactivity-warning.module.css +1 -0
  57. package/app/components/user/inactivity-warning.tsx +15 -2
  58. package/app/components/user/manage-profile.tsx +23 -10
  59. package/app/hooks/useOverlayDismiss.ts +52 -4
  60. package/app/routes/auth/login.tsx +785 -774
  61. package/app/routes/striae/striae.module.css +10 -3
  62. package/app/routes/striae/striae.tsx +469 -30
  63. package/app/services/audit/audit.service.ts +173 -27
  64. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  65. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  66. package/app/services/audit/builders/index.ts +1 -0
  67. package/app/types/audit.ts +3 -1
  68. package/app/types/case.ts +29 -0
  69. package/app/types/import.ts +3 -0
  70. package/app/utils/data/permissions.ts +16 -1
  71. package/app/utils/forensics/audit-export-signature.ts +5 -1
  72. package/app/utils/forensics/confirmation-signature.ts +3 -0
  73. package/app/utils/forensics/export-verification.ts +497 -22
  74. package/package.json +3 -3
  75. package/scripts/deploy-primershear-emails.sh +2 -1
  76. package/worker-configuration.d.ts +1 -1
  77. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  78. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  79. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  80. package/workers/data-worker/wrangler.jsonc.example +1 -1
  81. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  82. package/workers/image-worker/wrangler.jsonc.example +1 -1
  83. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  84. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  85. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  86. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  87. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  88. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  89. package/workers/user-worker/wrangler.jsonc.example +1 -1
  90. package/wrangler.toml.example +1 -1
  91. package/public/.well-known/keybase.txt +0 -56
@@ -0,0 +1,377 @@
1
+ import { useEffect, useRef, useState, useContext } from 'react';
2
+ import styles from './navbar.module.css';
3
+ import { SignOut } from '../actions/signout';
4
+ import { ManageProfile } from '../user/manage-profile';
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
+ import { AuthContext } from '~/contexts/auth.context';
9
+ import { getUserData } from '~/utils/data';
10
+ import { type ImportResult, type ConfirmationImportResult } from '~/types';
11
+
12
+ interface NavbarProps {
13
+ isUploading?: boolean;
14
+ company?: string;
15
+ isReadOnly?: boolean;
16
+ currentCase?: string;
17
+ currentFileName?: string;
18
+ isCurrentImageConfirmed?: boolean;
19
+ hasLoadedCase?: boolean;
20
+ hasLoadedImage?: boolean;
21
+ onImportComplete?: (result: ImportResult | ConfirmationImportResult) => void;
22
+ onOpenCase?: () => void;
23
+ onOpenListAllCases?: () => void;
24
+ onOpenCaseExport?: () => void;
25
+ onOpenAuditTrail?: () => void;
26
+ onOpenRenameCase?: () => void;
27
+ onDeleteCase?: () => void;
28
+ onArchiveCase?: () => void;
29
+ onOpenViewAllFiles?: () => void;
30
+ onDeleteCurrentFile?: () => void;
31
+ onOpenImageNotes?: () => void;
32
+ archiveDetails?: {
33
+ archived: boolean;
34
+ archivedAt?: string;
35
+ archivedBy?: string;
36
+ archivedByDisplay?: string;
37
+ archiveReason?: string;
38
+ };
39
+ }
40
+
41
+ export const Navbar = ({
42
+ isUploading = false,
43
+ company,
44
+ isReadOnly = false,
45
+ currentCase,
46
+ currentFileName,
47
+ isCurrentImageConfirmed = false,
48
+ hasLoadedCase = false,
49
+ hasLoadedImage = false,
50
+ onImportComplete,
51
+ onOpenCase,
52
+ onOpenListAllCases,
53
+ onOpenCaseExport,
54
+ onOpenAuditTrail,
55
+ onOpenRenameCase,
56
+ onDeleteCase,
57
+ onArchiveCase,
58
+ onOpenViewAllFiles,
59
+ onDeleteCurrentFile,
60
+ onOpenImageNotes,
61
+ archiveDetails,
62
+ }: NavbarProps) => {
63
+ const { user } = useContext(AuthContext);
64
+ const [userBadgeId, setUserBadgeId] = useState<string>('');
65
+ const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
66
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
67
+ const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
68
+ const [isCaseMenuOpen, setIsCaseMenuOpen] = useState(false);
69
+ const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
70
+ const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
71
+ const caseMenuRef = useRef<HTMLDivElement>(null);
72
+ const fileMenuRef = useRef<HTMLDivElement>(null);
73
+
74
+ useEffect(() => {
75
+ const loadUserBadgeId = async () => {
76
+ if (user) {
77
+ try {
78
+ const userData = await getUserData(user);
79
+ if (userData?.badgeId) {
80
+ setUserBadgeId(userData.badgeId);
81
+ }
82
+ } catch (err) {
83
+ console.error('Failed to load user badge ID:', err);
84
+ }
85
+ }
86
+ };
87
+
88
+ loadUserBadgeId();
89
+ }, [user]);
90
+
91
+ useEffect(() => {
92
+ if (!isCaseMenuOpen && !isFileMenuOpen) {
93
+ return;
94
+ }
95
+
96
+ const handlePointerDown = (event: MouseEvent) => {
97
+ const targetNode = event.target as Node;
98
+ const clickedOutsideCaseMenu = !caseMenuRef.current?.contains(targetNode);
99
+ const clickedOutsideFileMenu = !fileMenuRef.current?.contains(targetNode);
100
+
101
+ if (clickedOutsideCaseMenu) {
102
+ setIsCaseMenuOpen(false);
103
+ }
104
+
105
+ if (clickedOutsideFileMenu) {
106
+ setIsFileMenuOpen(false);
107
+ }
108
+ };
109
+
110
+ document.addEventListener('mousedown', handlePointerDown);
111
+ return () => {
112
+ document.removeEventListener('mousedown', handlePointerDown);
113
+ };
114
+ }, [isCaseMenuOpen, isFileMenuOpen]);
115
+
116
+ const caseActionsDisabled = false;
117
+ const isCaseManagementActive = true;
118
+ const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
119
+ const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed;
120
+ const isImageNotesActive = canOpenImageNotes;
121
+ const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
122
+
123
+ return (
124
+ <>
125
+ <header className={styles.navbar} aria-label="Canvas top navigation">
126
+ <div className={styles.companyLabelContainer}>
127
+ <div className={styles.companyLabel}>
128
+ {isReadOnly ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
129
+ </div>
130
+ </div>
131
+ <div className={styles.navCenterTrack}>
132
+ <div className={styles.navCentral}>
133
+ <div className={styles.caseMenuContainer} ref={caseMenuRef}>
134
+ <button
135
+ type="button"
136
+ className={`${styles.navSectionButton} ${isCaseManagementActive ? styles.navSectionButtonActive : ''}`}
137
+ aria-pressed={isCaseManagementActive}
138
+ aria-expanded={isCaseMenuOpen}
139
+ aria-haspopup="menu"
140
+ disabled={caseActionsDisabled}
141
+ onClick={() => setIsCaseMenuOpen((prev) => !prev)}
142
+ title={isUploading ? 'Cannot access case actions while uploading' : undefined}
143
+ >
144
+ Case Management
145
+ </button>
146
+ {isCaseMenuOpen && (
147
+ <div className={styles.caseMenu} role="menu" aria-label="Case Management actions">
148
+ <div className={styles.caseMenuSectionLabel}>Case Access</div>
149
+ <button
150
+ type="button"
151
+ role="menuitem"
152
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemOpen}`}
153
+ onClick={() => {
154
+ onOpenCase?.();
155
+ setIsCaseMenuOpen(false);
156
+ }}
157
+ >
158
+ Open Case
159
+ </button>
160
+ <button
161
+ type="button"
162
+ role="menuitem"
163
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemList}`}
164
+ onClick={() => {
165
+ onOpenListAllCases?.();
166
+ setIsCaseMenuOpen(false);
167
+ }}
168
+ >
169
+ List All Cases
170
+ </button>
171
+ <div className={styles.caseMenuSectionLabel}>Case Operations</div>
172
+ <button
173
+ type="button"
174
+ role="menuitem"
175
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
176
+ disabled={!hasLoadedCase}
177
+ title={!hasLoadedCase ? 'Load a case to export case data' : undefined}
178
+ onClick={() => {
179
+ onOpenCaseExport?.();
180
+ setIsCaseMenuOpen(false);
181
+ }}
182
+ >
183
+ Export Case Data
184
+ </button>
185
+ <button
186
+ type="button"
187
+ role="menuitem"
188
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemAudit}`}
189
+ disabled={!hasLoadedCase}
190
+ title={!hasLoadedCase ? 'Load a case to view audit trail' : undefined}
191
+ onClick={() => {
192
+ onOpenAuditTrail?.();
193
+ setIsCaseMenuOpen(false);
194
+ }}
195
+ >
196
+ Case Audit Trail
197
+ </button>
198
+ {(!isReadOnly || archiveDetails?.archived) && (
199
+ <div className={styles.caseMenuSectionLabel}>Maintenance</div>
200
+ )}
201
+ {!isReadOnly && (
202
+ <button
203
+ type="button"
204
+ role="menuitem"
205
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemRename}`}
206
+ disabled={!hasLoadedCase}
207
+ title={!hasLoadedCase ? 'Load a case to rename it' : undefined}
208
+ onClick={() => {
209
+ onOpenRenameCase?.();
210
+ setIsCaseMenuOpen(false);
211
+ }}
212
+ >
213
+ Rename Case
214
+ </button>
215
+ )}
216
+ {(!isReadOnly || archiveDetails?.archived) && (
217
+ <button
218
+ type="button"
219
+ role="menuitem"
220
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemDelete}`}
221
+ disabled={!hasLoadedCase}
222
+ title={!hasLoadedCase ? 'Load a case to delete it' : undefined}
223
+ onClick={() => {
224
+ onDeleteCase?.();
225
+ setIsCaseMenuOpen(false);
226
+ }}
227
+ >
228
+ Delete Case
229
+ </button>
230
+ )}
231
+ {!isReadOnly && (
232
+ <button
233
+ type="button"
234
+ role="menuitem"
235
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemArchive}`}
236
+ disabled={!hasLoadedCase}
237
+ title={!hasLoadedCase ? 'Load a case to archive it' : undefined}
238
+ onClick={() => {
239
+ onArchiveCase?.();
240
+ setIsCaseMenuOpen(false);
241
+ }}
242
+ >
243
+ Archive Case
244
+ </button>
245
+ )}
246
+ <div className={styles.caseMenuSectionLabel}>Verification</div>
247
+ <button
248
+ type="button"
249
+ role="menuitem"
250
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
251
+ onClick={() => {
252
+ setIsPublicKeyModalOpen(true);
253
+ setIsCaseMenuOpen(false);
254
+ }}
255
+ >
256
+ Verify Exports
257
+ </button>
258
+ {currentCase && (
259
+ <div className={styles.caseMenuCaption}>Case: {currentCase}</div>
260
+ )}
261
+ {archiveDetails?.archived && (
262
+ <div className={styles.caseArchiveDetails}>
263
+ <strong>Archived Case</strong>
264
+ <span>Archived At: {archiveDetails.archivedAt ? new Date(archiveDetails.archivedAt).toLocaleString() : 'Unknown'}</span>
265
+ <span>
266
+ Archived By (Name, ID): {archiveDetails.archivedByDisplay || archiveDetails.archivedBy || 'Unknown'}
267
+ {archiveDetails.archivedByDisplay && archiveDetails.archivedBy ? ` (${archiveDetails.archivedBy})` : ''}
268
+ </span>
269
+ <span>Reason: {archiveDetails.archiveReason || 'Not provided'}</span>
270
+ </div>
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ <div className={styles.fileMenuContainer} ref={fileMenuRef}>
276
+ <button
277
+ type="button"
278
+ className={`${styles.navSectionButton} ${isFileManagementActive ? styles.navSectionButtonActive : ''}`}
279
+ disabled={!hasLoadedCase}
280
+ aria-pressed={isFileManagementActive}
281
+ aria-expanded={isFileMenuOpen}
282
+ aria-haspopup="menu"
283
+ onClick={() => setIsFileMenuOpen((prev) => !prev)}
284
+ title={!hasLoadedCase ? 'Load a case to enable file management' : undefined}
285
+ >
286
+ File Management
287
+ </button>
288
+ {isFileMenuOpen && (
289
+ <div className={styles.fileMenu} role="menu" aria-label="File Management actions">
290
+ <button
291
+ type="button"
292
+ role="menuitem"
293
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemViewAll}`}
294
+ onClick={() => {
295
+ onOpenViewAllFiles?.();
296
+ setIsFileMenuOpen(false);
297
+ }}
298
+ >
299
+ View All Files
300
+ </button>
301
+ <div className={styles.fileMenuSectionLabel}>Selected File</div>
302
+ <button
303
+ type="button"
304
+ role="menuitem"
305
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemDelete}`}
306
+ disabled={!canDeleteCurrentFile}
307
+ title={!hasLoadedImage ? 'Load an image to delete the selected file' : isReadOnly ? 'Cannot delete files for read-only cases' : undefined}
308
+ onClick={() => {
309
+ onDeleteCurrentFile?.();
310
+ setIsFileMenuOpen(false);
311
+ }}
312
+ >
313
+ Delete File
314
+ </button>
315
+ <div
316
+ className={styles.fileMenuCaption}
317
+ title={hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
318
+ >
319
+ File: {hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
320
+ </div>
321
+ </div>
322
+ )}
323
+ </div>
324
+ <button
325
+ type="button"
326
+ className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
327
+ disabled={!canOpenImageNotes}
328
+ aria-pressed={isImageNotesActive}
329
+ title={!hasLoadedImage ? 'Load an image to enable image notes' : isCurrentImageConfirmed ? 'Confirmed images are read-only and viewable via toolbar only' : undefined}
330
+ onClick={() => {
331
+ onOpenImageNotes?.();
332
+ }}
333
+ >
334
+ Image Notes
335
+ </button>
336
+ <button
337
+ type="button"
338
+ onClick={() => setIsImportModalOpen(true)}
339
+ className={`${styles.navSectionButton} ${styles.navPrimaryButton}`}
340
+ disabled={isUploading}
341
+ title={isUploading ? 'Cannot import while uploading files' : undefined}
342
+ >
343
+ Import Case/Confirmations
344
+ </button>
345
+ </div>
346
+ </div>
347
+ <div className={styles.navActions}>
348
+ <button
349
+ type="button"
350
+ onClick={() => setIsProfileModalOpen(true)}
351
+ className={styles.navTextButton}
352
+ disabled={isUploading}
353
+ title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
354
+ >
355
+ Manage Profile
356
+ </button>
357
+ <SignOut disabled={isUploading} />
358
+ </div>
359
+ </header>
360
+ <CaseImport
361
+ isOpen={isImportModalOpen}
362
+ onClose={() => setIsImportModalOpen(false)}
363
+ onImportComplete={onImportComplete}
364
+ />
365
+ <ManageProfile
366
+ isOpen={isProfileModalOpen}
367
+ onClose={() => setIsProfileModalOpen(false)}
368
+ />
369
+ <PublicSigningKeyModal
370
+ isOpen={isPublicKeyModalOpen}
371
+ onClose={() => setIsPublicKeyModalOpen(false)}
372
+ publicSigningKeyId={publicSigningKeyId}
373
+ publicKeyPem={publicKeyPem}
374
+ />
375
+ </>
376
+ );
377
+ };
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  width: 100%;
15
16
  max-width: 640px;
16
17
  max-height: 90vh;
@@ -230,8 +230,8 @@ export const PublicSigningKeyModal = ({
230
230
  const publicKeyInputId = useId();
231
231
  const exportFileInputId = useId();
232
232
  const {
233
- handleOverlayMouseDown,
234
- handleOverlayKeyDown
233
+ overlayProps,
234
+ getCloseButtonProps
235
235
  } = useOverlayDismiss({
236
236
  isOpen,
237
237
  onClose
@@ -303,7 +303,7 @@ export const PublicSigningKeyModal = ({
303
303
  const lowerName = file.name.toLowerCase();
304
304
 
305
305
  if (!lowerName.endsWith('.zip') && !lowerName.endsWith('.json')) {
306
- setExportFileError('Select a confirmation JSON/ZIP file or a case export ZIP file.');
306
+ setExportFileError('Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
307
307
  return;
308
308
  }
309
309
 
@@ -317,7 +317,7 @@ export const PublicSigningKeyModal = ({
317
317
  const hasExportFile = !!selectedExportFile;
318
318
 
319
319
  setKeyError(hasPublicKey ? '' : 'Select or download a public key PEM file first.');
320
- setExportFileError(hasExportFile ? '' : 'Select a confirmation JSON/ZIP file or a case export ZIP file.');
320
+ setExportFileError(hasExportFile ? '' : 'Select a confirmation JSON/ZIP file, standalone audit JSON export, or a case export ZIP file.');
321
321
 
322
322
  if (!hasPublicKey || !hasExportFile || !selectedPublicKey || !selectedExportFile) {
323
323
  return;
@@ -350,18 +350,19 @@ export const PublicSigningKeyModal = ({
350
350
  return lowerName.includes('confirmation-data-') ? 'Confirmation ZIP' : 'Case export ZIP';
351
351
  }
352
352
 
353
- return 'Confirmation JSON';
353
+ if (lowerName.includes('audit')) {
354
+ return 'Audit JSON';
355
+ }
356
+
357
+ return 'JSON export';
354
358
  })()} • ${formatFileSize(selectedExportFile.size)}`
355
359
  : undefined;
356
360
 
357
361
  return (
358
362
  <div
359
363
  className={styles.overlay}
360
- onMouseDown={handleOverlayMouseDown}
361
- onKeyDown={handleOverlayKeyDown}
362
- role="button"
363
- tabIndex={0}
364
364
  aria-label="Close public signing key dialog"
365
+ {...overlayProps}
365
366
  >
366
367
  <div
367
368
  className={styles.modal}
@@ -371,13 +372,11 @@ export const PublicSigningKeyModal = ({
371
372
  >
372
373
  <div className={styles.header}>
373
374
  <h3 id={publicSigningKeyTitleId} className={styles.title}>
374
- Striae Public Signing Key
375
+ Striae Verification Utility
375
376
  </h3>
376
377
  <button
377
- type="button"
378
378
  className={styles.closeButton}
379
- onClick={onClose}
380
- aria-label="Close public signing key dialog"
379
+ {...getCloseButtonProps({ ariaLabel: 'Close public signing key dialog' })}
381
380
  >
382
381
  &times;
383
382
  </button>
@@ -385,7 +384,7 @@ export const PublicSigningKeyModal = ({
385
384
 
386
385
  <div className={styles.content}>
387
386
  <p className={styles.description}>
388
- Drop a public key PEM file and a Striae confirmation JSON/ZIP or case export ZIP, then run
387
+ Drop a public key PEM file and a Striae confirmation JSON/ZIP, standalone audit JSON export, or case export ZIP, then run
389
388
  verification directly in the browser.
390
389
  </p>
391
390
 
@@ -422,8 +421,8 @@ export const PublicSigningKeyModal = ({
422
421
  inputId={exportFileInputId}
423
422
  label="2. Confirmation File or Export ZIP"
424
423
  accept=".json,.zip"
425
- emptyText="Drop a confirmation JSON/ZIP or case export ZIP here"
426
- helperText="Case exports use .zip. Confirmation exports can be .json or .zip."
424
+ emptyText="Drop a confirmation JSON/ZIP, audit JSON, or case export ZIP here"
425
+ helperText="Case exports use .zip. Confirmation exports can be .json or .zip. Audit exports are supported as standalone .json files."
427
426
  selectedFileName={selectedExportFile?.name}
428
427
  selectedDescription={selectedExportDescription}
429
428
  errorMessage={exportFileError}
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  background: var(--backgroundLight);
15
16
  border-radius: var(--spaceXS);
16
17
  width: 90%;
@@ -2,8 +2,6 @@ import { useState, useEffect, useContext } from 'react';
2
2
  import styles from './case-export.module.css';
3
3
  import { AuthContext } from '~/contexts/auth.context';
4
4
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
5
- import { PublicSigningKeyModal } from '~/components/public-signing-key-modal/public-signing-key-modal';
6
- import { getCurrentPublicSigningKeyDetails } from '~/utils/forensics';
7
5
  import { getCaseConfirmations, exportConfirmationData } from '../../actions/confirm-export';
8
6
 
9
7
  export type ExportFormat = 'json' | 'csv';
@@ -35,15 +33,13 @@ export const CaseExport = ({
35
33
  const [selectedFormat, setSelectedFormat] = useState<ExportFormat>('json');
36
34
  const [includeImages, setIncludeImages] = useState(false);
37
35
  const [hasConfirmationData, setHasConfirmationData] = useState(false);
38
- const [isPublicKeyModalOpen, setIsPublicKeyModalOpen] = useState(false);
39
- const { keyId: publicSigningKeyId, publicKeyPem } = getCurrentPublicSigningKeyDetails();
40
36
  const {
41
- handleOverlayMouseDown,
42
- handleOverlayKeyDown
37
+ requestClose,
38
+ overlayProps,
39
+ getCloseButtonProps
43
40
  } = useOverlayDismiss({
44
41
  isOpen,
45
42
  onClose,
46
- canDismiss: !isPublicKeyModalOpen
47
43
  });
48
44
 
49
45
  // Update caseNumber when currentCaseNumber prop changes
@@ -103,12 +99,6 @@ export const CaseExport = ({
103
99
  }
104
100
  }, [isReadOnly]);
105
101
 
106
- useEffect(() => {
107
- if (!isOpen) {
108
- setIsPublicKeyModalOpen(false);
109
- }
110
- }, [isOpen]);
111
-
112
102
  if (!isOpen) return null;
113
103
 
114
104
  const handleExport = async () => {
@@ -125,7 +115,7 @@ export const CaseExport = ({
125
115
  await onExport(caseNumber.trim(), selectedFormat, includeImages, (progress, label) => {
126
116
  setExportProgress({ current: progress, total: 100, caseName: label, mode: 'single' });
127
117
  });
128
- onClose();
118
+ requestClose();
129
119
  } catch (error) {
130
120
  console.error('Export failed:', error);
131
121
  setError(error instanceof Error ? error.message : 'Export failed. Please try again.');
@@ -144,7 +134,7 @@ export const CaseExport = ({
144
134
  await onExportAll((current: number, total: number, caseName: string) => {
145
135
  setExportProgress({ current, total, caseName });
146
136
  }, selectedFormat);
147
- onClose();
137
+ requestClose();
148
138
  } catch (error) {
149
139
  console.error('Export all failed:', error);
150
140
  setError(error instanceof Error ? error.message : 'Export all cases failed. Please try again.');
@@ -165,7 +155,7 @@ export const CaseExport = ({
165
155
 
166
156
  try {
167
157
  await exportConfirmationData(user, caseNumber.trim());
168
- onClose();
158
+ requestClose();
169
159
  } catch (error) {
170
160
  console.error('Confirmation export failed:', error);
171
161
  setError(error instanceof Error ? error.message : 'Confirmation export failed. Please try again.');
@@ -177,20 +167,13 @@ export const CaseExport = ({
177
167
  return (
178
168
  <div
179
169
  className={styles.overlay}
180
- onMouseDown={handleOverlayMouseDown}
181
- onKeyDown={handleOverlayKeyDown}
182
- role="button"
183
- tabIndex={0}
184
170
  aria-label="Close case export dialog"
171
+ {...overlayProps}
185
172
  >
186
173
  <div className={styles.modal}>
187
174
  <div className={styles.header}>
188
175
  <h2 className={styles.title}>Export Case Data</h2>
189
- <button
190
- className={styles.closeButton}
191
- onClick={onClose}
192
- aria-label="Close modal"
193
- >
176
+ <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close case export dialog' })}>
194
177
  ×
195
178
  </button>
196
179
  </div>
@@ -313,20 +296,6 @@ export const CaseExport = ({
313
296
  </div>
314
297
  </div>
315
298
  )}
316
-
317
- <div className={styles.divider}>
318
- <span>Verification</span>
319
- </div>
320
-
321
- <div className={styles.publicKeySection}>
322
- <button
323
- type="button"
324
- className={styles.publicKeyButton}
325
- onClick={() => setIsPublicKeyModalOpen(true)}
326
- >
327
- View Public Signing Key
328
- </button>
329
- </div>
330
299
 
331
300
  {error && (
332
301
  <div className={styles.error}>
@@ -336,13 +305,6 @@ export const CaseExport = ({
336
305
  </div>
337
306
  </div>
338
307
  </div>
339
-
340
- <PublicSigningKeyModal
341
- isOpen={isPublicKeyModalOpen}
342
- onClose={() => setIsPublicKeyModalOpen(false)}
343
- publicSigningKeyId={publicSigningKeyId}
344
- publicKeyPem={publicKeyPem}
345
- />
346
308
  </div>
347
309
  );
348
310
  };
@@ -11,6 +11,7 @@
11
11
  }
12
12
 
13
13
  .modal {
14
+ position: relative;
14
15
  background: var(--backgroundLight);
15
16
  border-radius: var(--spaceXS);
16
17
  width: 90%;
@@ -424,6 +425,28 @@
424
425
  color: var(--textTitle);
425
426
  }
426
427
 
428
+ .archivedImportNote {
429
+ margin-bottom: var(--spaceM);
430
+ padding: var(--spaceS) var(--spaceM);
431
+ border-radius: var(--spaceXS);
432
+ background: color-mix(in lab, var(--success) 10%, transparent);
433
+ border: 1px solid color-mix(in lab, var(--success) 25%, transparent);
434
+ color: color-mix(in lab, var(--success) 80%, var(--black));
435
+ font-size: var(--fontSizeBodyXS);
436
+ font-weight: var(--fontWeightMedium);
437
+ }
438
+
439
+ .archivedRegularCaseRiskNote {
440
+ margin-bottom: var(--spaceM);
441
+ padding: var(--spaceS) var(--spaceM);
442
+ border-radius: var(--spaceXS);
443
+ background: color-mix(in lab, var(--warning) 12%, transparent);
444
+ border: 1px solid color-mix(in lab, var(--warning) 30%, transparent);
445
+ color: color-mix(in lab, var(--warning) 85%, var(--black));
446
+ font-size: var(--fontSizeBodyXS);
447
+ font-weight: var(--fontWeightMedium);
448
+ }
449
+
427
450
  /* Validation Section - Green/Red Based on Status */
428
451
  .validationSection {
429
452
  border-radius: var(--spaceXS);