@striae-org/striae 4.1.0 → 4.2.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.
Files changed (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -0,0 +1,402 @@
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 disableLongRunningCaseActions = isUploading;
118
+ const isCaseManagementActive = true;
119
+ const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
120
+ const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed;
121
+ const isImageNotesActive = canOpenImageNotes;
122
+ const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
123
+
124
+ return (
125
+ <>
126
+ <header className={styles.navbar} aria-label="Canvas top navigation">
127
+ <div className={styles.companyLabelContainer}>
128
+ <div className={styles.companyLabel}>
129
+ {isReadOnly ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
130
+ </div>
131
+ </div>
132
+ <div className={styles.navCenterTrack}>
133
+ <div className={styles.navCentral}>
134
+ <div className={styles.caseMenuContainer} ref={caseMenuRef}>
135
+ <button
136
+ type="button"
137
+ className={`${styles.navSectionButton} ${isCaseManagementActive ? styles.navSectionButtonActive : ''}`}
138
+ aria-pressed={isCaseManagementActive}
139
+ aria-expanded={isCaseMenuOpen}
140
+ aria-haspopup="menu"
141
+ disabled={caseActionsDisabled}
142
+ onClick={() => setIsCaseMenuOpen((prev) => !prev)}
143
+ title={isUploading ? 'Some case actions are unavailable while files are uploading' : undefined}
144
+ >
145
+ Case Management
146
+ </button>
147
+ {isCaseMenuOpen && (
148
+ <div className={styles.caseMenu} role="menu" aria-label="Case Management actions">
149
+ <div className={styles.caseMenuSectionLabel}>Case Access</div>
150
+ <button
151
+ type="button"
152
+ role="menuitem"
153
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemOpen}`}
154
+ onClick={() => {
155
+ onOpenCase?.();
156
+ setIsCaseMenuOpen(false);
157
+ }}
158
+ >
159
+ Open Case
160
+ </button>
161
+ <button
162
+ type="button"
163
+ role="menuitem"
164
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemList}`}
165
+ onClick={() => {
166
+ onOpenListAllCases?.();
167
+ setIsCaseMenuOpen(false);
168
+ }}
169
+ >
170
+ List All Cases
171
+ </button>
172
+ <div className={styles.caseMenuSectionLabel}>Case Operations</div>
173
+ <button
174
+ type="button"
175
+ role="menuitem"
176
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
177
+ disabled={!hasLoadedCase || disableLongRunningCaseActions}
178
+ title={
179
+ !hasLoadedCase
180
+ ? 'Load a case to export case data'
181
+ : disableLongRunningCaseActions
182
+ ? 'Export is unavailable while files are uploading'
183
+ : undefined
184
+ }
185
+ onClick={() => {
186
+ onOpenCaseExport?.();
187
+ setIsCaseMenuOpen(false);
188
+ }}
189
+ >
190
+ Export Case Data
191
+ </button>
192
+ <button
193
+ type="button"
194
+ role="menuitem"
195
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemAudit}`}
196
+ disabled={!hasLoadedCase}
197
+ title={!hasLoadedCase ? 'Load a case to view audit trail' : undefined}
198
+ onClick={() => {
199
+ onOpenAuditTrail?.();
200
+ setIsCaseMenuOpen(false);
201
+ }}
202
+ >
203
+ Case Audit Trail
204
+ </button>
205
+ {(!isReadOnly || archiveDetails?.archived) && (
206
+ <div className={styles.caseMenuSectionLabel}>Maintenance</div>
207
+ )}
208
+ {!isReadOnly && (
209
+ <button
210
+ type="button"
211
+ role="menuitem"
212
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemRename}`}
213
+ disabled={!hasLoadedCase || disableLongRunningCaseActions}
214
+ title={
215
+ !hasLoadedCase
216
+ ? 'Load a case to rename it'
217
+ : disableLongRunningCaseActions
218
+ ? 'Rename is unavailable while files are uploading'
219
+ : undefined
220
+ }
221
+ onClick={() => {
222
+ onOpenRenameCase?.();
223
+ setIsCaseMenuOpen(false);
224
+ }}
225
+ >
226
+ Rename Case
227
+ </button>
228
+ )}
229
+ {(!isReadOnly || archiveDetails?.archived) && (
230
+ <button
231
+ type="button"
232
+ role="menuitem"
233
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemDelete}`}
234
+ disabled={!hasLoadedCase || disableLongRunningCaseActions}
235
+ title={
236
+ !hasLoadedCase
237
+ ? 'Load a case to delete it'
238
+ : disableLongRunningCaseActions
239
+ ? 'Delete is unavailable while files are uploading'
240
+ : undefined
241
+ }
242
+ onClick={() => {
243
+ onDeleteCase?.();
244
+ setIsCaseMenuOpen(false);
245
+ }}
246
+ >
247
+ Delete Case
248
+ </button>
249
+ )}
250
+ {!isReadOnly && (
251
+ <button
252
+ type="button"
253
+ role="menuitem"
254
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemArchive}`}
255
+ disabled={!hasLoadedCase || disableLongRunningCaseActions}
256
+ title={
257
+ !hasLoadedCase
258
+ ? 'Load a case to archive it'
259
+ : disableLongRunningCaseActions
260
+ ? 'Archive is unavailable while files are uploading'
261
+ : undefined
262
+ }
263
+ onClick={() => {
264
+ onArchiveCase?.();
265
+ setIsCaseMenuOpen(false);
266
+ }}
267
+ >
268
+ Archive Case
269
+ </button>
270
+ )}
271
+ <div className={styles.caseMenuSectionLabel}>Verification</div>
272
+ <button
273
+ type="button"
274
+ role="menuitem"
275
+ className={`${styles.caseMenuItem} ${styles.caseMenuItemKey}`}
276
+ onClick={() => {
277
+ setIsPublicKeyModalOpen(true);
278
+ setIsCaseMenuOpen(false);
279
+ }}
280
+ >
281
+ Verify Exports
282
+ </button>
283
+ {currentCase && (
284
+ <div className={styles.caseMenuCaption}>Case: {currentCase}</div>
285
+ )}
286
+ {archiveDetails?.archived && (
287
+ <div className={styles.caseArchiveDetails}>
288
+ <strong>Archived Case</strong>
289
+ <span>Archived At: {archiveDetails.archivedAt ? new Date(archiveDetails.archivedAt).toLocaleString() : 'Unknown'}</span>
290
+ <span>
291
+ Archived By (Name, ID): {archiveDetails.archivedByDisplay || archiveDetails.archivedBy || 'Unknown'}
292
+ {archiveDetails.archivedByDisplay && archiveDetails.archivedBy ? ` (${archiveDetails.archivedBy})` : ''}
293
+ </span>
294
+ <span>Reason: {archiveDetails.archiveReason || 'Not provided'}</span>
295
+ </div>
296
+ )}
297
+ </div>
298
+ )}
299
+ </div>
300
+ <div className={styles.fileMenuContainer} ref={fileMenuRef}>
301
+ <button
302
+ type="button"
303
+ className={`${styles.navSectionButton} ${isFileManagementActive ? styles.navSectionButtonActive : ''}`}
304
+ disabled={!hasLoadedCase}
305
+ aria-pressed={isFileManagementActive}
306
+ aria-expanded={isFileMenuOpen}
307
+ aria-haspopup="menu"
308
+ onClick={() => setIsFileMenuOpen((prev) => !prev)}
309
+ title={!hasLoadedCase ? 'Load a case to enable file management' : undefined}
310
+ >
311
+ File Management
312
+ </button>
313
+ {isFileMenuOpen && (
314
+ <div className={styles.fileMenu} role="menu" aria-label="File Management actions">
315
+ <button
316
+ type="button"
317
+ role="menuitem"
318
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemViewAll}`}
319
+ onClick={() => {
320
+ onOpenViewAllFiles?.();
321
+ setIsFileMenuOpen(false);
322
+ }}
323
+ >
324
+ View All Files
325
+ </button>
326
+ <div className={styles.fileMenuSectionLabel}>Selected File</div>
327
+ <button
328
+ type="button"
329
+ role="menuitem"
330
+ className={`${styles.fileMenuItem} ${styles.fileMenuItemDelete}`}
331
+ disabled={!canDeleteCurrentFile}
332
+ title={!hasLoadedImage ? 'Load an image to delete the selected file' : isReadOnly ? 'Cannot delete files for read-only cases' : undefined}
333
+ onClick={() => {
334
+ onDeleteCurrentFile?.();
335
+ setIsFileMenuOpen(false);
336
+ }}
337
+ >
338
+ Delete File
339
+ </button>
340
+ <div
341
+ className={styles.fileMenuCaption}
342
+ title={hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
343
+ >
344
+ File: {hasLoadedImage && currentFileName ? currentFileName : 'No file loaded'}
345
+ </div>
346
+ </div>
347
+ )}
348
+ </div>
349
+ <button
350
+ type="button"
351
+ className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
352
+ disabled={!canOpenImageNotes}
353
+ aria-pressed={isImageNotesActive}
354
+ title={!hasLoadedImage ? 'Load an image to enable image notes' : isCurrentImageConfirmed ? 'Confirmed images are read-only and viewable via toolbar only' : undefined}
355
+ onClick={() => {
356
+ onOpenImageNotes?.();
357
+ }}
358
+ >
359
+ Image Notes
360
+ </button>
361
+ <button
362
+ type="button"
363
+ onClick={() => setIsImportModalOpen(true)}
364
+ className={`${styles.navSectionButton} ${styles.navPrimaryButton}`}
365
+ disabled={isUploading}
366
+ title={isUploading ? 'Cannot import while uploading files' : undefined}
367
+ >
368
+ Import Case/Confirmations
369
+ </button>
370
+ </div>
371
+ </div>
372
+ <div className={styles.navActions}>
373
+ <button
374
+ type="button"
375
+ onClick={() => setIsProfileModalOpen(true)}
376
+ className={styles.navTextButton}
377
+ disabled={isUploading}
378
+ title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
379
+ >
380
+ Manage Profile
381
+ </button>
382
+ <SignOut disabled={isUploading} />
383
+ </div>
384
+ </header>
385
+ <CaseImport
386
+ isOpen={isImportModalOpen}
387
+ onClose={() => setIsImportModalOpen(false)}
388
+ onImportComplete={onImportComplete}
389
+ />
390
+ <ManageProfile
391
+ isOpen={isProfileModalOpen}
392
+ onClose={() => setIsProfileModalOpen(false)}
393
+ />
394
+ <PublicSigningKeyModal
395
+ isOpen={isPublicKeyModalOpen}
396
+ onClose={() => setIsPublicKeyModalOpen(false)}
397
+ publicSigningKeyId={publicSigningKeyId}
398
+ publicKeyPem={publicKeyPem}
399
+ />
400
+ </>
401
+ );
402
+ };
@@ -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);