@striae-org/striae 4.2.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/app/components/actions/case-manage.ts +50 -17
  3. package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
  4. package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
  5. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  6. package/app/components/canvas/confirmation/confirmation.tsx +6 -2
  7. package/app/components/colors/colors.module.css +4 -3
  8. package/app/components/navbar/navbar.tsx +34 -9
  9. package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
  10. package/app/components/sidebar/cases/cases-modal.tsx +76 -35
  11. package/app/components/sidebar/cases/cases.module.css +20 -0
  12. package/app/components/sidebar/files/files-modal.tsx +37 -39
  13. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  14. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
  15. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  16. package/app/components/sidebar/notes/notes.module.css +27 -11
  17. package/app/components/sidebar/sidebar-container.tsx +1 -0
  18. package/app/components/sidebar/sidebar.tsx +3 -0
  19. package/app/{tailwind.css → global.css} +1 -3
  20. package/app/hooks/useOverlayDismiss.ts +6 -4
  21. package/app/root.tsx +1 -1
  22. package/app/routes/striae/striae.tsx +6 -0
  23. package/app/services/audit/audit.service.ts +2 -2
  24. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  25. package/app/types/audit.ts +1 -0
  26. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  27. package/app/utils/data/data-operations.ts +17 -861
  28. package/app/utils/data/index.ts +11 -1
  29. package/app/utils/data/operations/batch-operations.ts +113 -0
  30. package/app/utils/data/operations/case-operations.ts +168 -0
  31. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  32. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  33. package/app/utils/data/operations/index.ts +7 -0
  34. package/app/utils/data/operations/signing-operations.ts +225 -0
  35. package/app/utils/data/operations/types.ts +42 -0
  36. package/app/utils/data/operations/validation-operations.ts +48 -0
  37. package/app/utils/forensics/export-verification.ts +40 -111
  38. package/functions/api/_shared/firebase-auth.ts +2 -7
  39. package/functions/api/image/[[path]].ts +20 -23
  40. package/functions/api/pdf/[[path]].ts +27 -8
  41. package/package.json +5 -10
  42. package/scripts/deploy-primershear-emails.sh +1 -1
  43. package/worker-configuration.d.ts +2 -2
  44. package/workers/audit-worker/package.json +1 -1
  45. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  46. package/workers/data-worker/package.json +1 -1
  47. package/workers/data-worker/wrangler.jsonc.example +1 -1
  48. package/workers/image-worker/package.json +1 -1
  49. package/workers/image-worker/src/image-worker.example.ts +16 -5
  50. package/workers/image-worker/wrangler.jsonc.example +1 -1
  51. package/workers/keys-worker/package.json +1 -1
  52. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  53. package/workers/pdf-worker/package.json +1 -1
  54. package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
  55. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  56. package/workers/pdf-worker/src/report-types.ts +3 -3
  57. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  58. package/workers/user-worker/package.json +1 -1
  59. package/workers/user-worker/src/user-worker.example.ts +17 -0
  60. package/workers/user-worker/wrangler.jsonc.example +1 -1
  61. package/wrangler.toml.example +1 -1
  62. package/NOTICE +0 -13
  63. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  64. package/postcss.config.js +0 -6
  65. package/tailwind.config.ts +0 -22
@@ -732,6 +732,9 @@ export const Striae = ({ user }: StriaePage) => {
732
732
  <SidebarContainer
733
733
  user={user}
734
734
  onImageSelect={handleImageSelect}
735
+ onOpenCase={() => {
736
+ void handleOpenCaseModal();
737
+ }}
735
738
  imageId={imageId}
736
739
  currentCase={currentCase}
737
740
  imageLoaded={imageLoaded}
@@ -797,6 +800,7 @@ export const Striae = ({ user }: StriaePage) => {
797
800
  }}
798
801
  currentCase={currentCase || ''}
799
802
  user={user}
803
+ confirmationSaveVersion={confirmationSaveVersion}
800
804
  />
801
805
  <FilesModal
802
806
  isOpen={isFilesModalOpen}
@@ -809,6 +813,7 @@ export const Striae = ({ user }: StriaePage) => {
809
813
  setFiles={setFiles}
810
814
  isReadOnly={isReadOnlyCase}
811
815
  selectedFileId={imageId}
816
+ confirmationSaveVersion={confirmationSaveVersion}
812
817
  />
813
818
  <NotesEditorModal
814
819
  isOpen={showNotes}
@@ -819,6 +824,7 @@ export const Striae = ({ user }: StriaePage) => {
819
824
  onAnnotationRefresh={refreshAnnotationData}
820
825
  originalFileName={files.find(file => file.id === imageId)?.originalFilename}
821
826
  isUploading={isUploading}
827
+ showNotification={showNotification}
822
828
  />
823
829
  <CaseExport
824
830
  isOpen={isCaseExportModalOpen}
@@ -1067,7 +1067,7 @@ export class AuditService {
1067
1067
 
1068
1068
  const bundledEntries = caseData.bundledAuditTrail?.entries;
1069
1069
  if (!Array.isArray(bundledEntries)) {
1070
- return [];
1070
+ return null;
1071
1071
  }
1072
1072
 
1073
1073
  const sortedEntries = sortAuditEntriesNewestFirst(bundledEntries);
@@ -1087,7 +1087,7 @@ export class AuditService {
1087
1087
  private async getAuditEntries(params: AuditQueryParams, requestingUser?: User): Promise<ValidationAuditEntry[]> {
1088
1088
  try {
1089
1089
  const bundledArchivedEntries = await this.getBundledArchivedCaseAuditEntries(params, requestingUser);
1090
- if (bundledArchivedEntries) {
1090
+ if (bundledArchivedEntries !== null) {
1091
1091
  return bundledArchivedEntries;
1092
1092
  }
1093
1093
 
@@ -118,7 +118,7 @@ export const buildCaseArchiveAuditParams = (
118
118
  workflowPhase: 'casework',
119
119
  caseDetails: {
120
120
  newCaseName: input.caseName,
121
- deleteReason: input.archiveReason,
121
+ archiveReason: input.archiveReason,
122
122
  totalFiles: input.totalFiles,
123
123
  lastModified: archivedAt,
124
124
  },
@@ -202,6 +202,7 @@ export interface CaseAuditDetails {
202
202
  createdDate?: string;
203
203
  lastModified?: string;
204
204
  deleteReason?: string;
205
+ archiveReason?: string;
205
206
  backupCreated?: boolean;
206
207
  }
207
208
 
@@ -0,0 +1,279 @@
1
+ import type { User } from 'firebase/auth';
2
+ import { type AnnotationData } from '~/types';
3
+
4
+ export interface FileConfirmationSummary {
5
+ includeConfirmation: boolean;
6
+ isConfirmed: boolean;
7
+ updatedAt: string;
8
+ }
9
+
10
+ export interface CaseConfirmationSummary {
11
+ includeConfirmation: boolean;
12
+ isConfirmed: boolean;
13
+ updatedAt: string;
14
+ filesById: Record<string, FileConfirmationSummary>;
15
+ }
16
+
17
+ export interface UserConfirmationSummaryDocument {
18
+ version: number;
19
+ updatedAt: string;
20
+ cases: Record<string, CaseConfirmationSummary>;
21
+ }
22
+
23
+ export interface ConfirmationSummaryEnsureOptions {
24
+ forceRefresh?: boolean;
25
+ maxAgeMs?: number;
26
+ }
27
+
28
+ export interface ConfirmationSummaryTelemetry {
29
+ ensureCalls: number;
30
+ caseCacheHits: number;
31
+ caseMisses: number;
32
+ forceRefreshCalls: number;
33
+ staleCaseRefreshes: number;
34
+ staleFileRefreshes: number;
35
+ missingFileRefreshes: number;
36
+ removedFileEntries: number;
37
+ refreshedFileEntries: number;
38
+ summaryWrites: number;
39
+ }
40
+
41
+ export const CONFIRMATION_SUMMARY_VERSION = 1;
42
+ export const DEFAULT_CONFIRMATION_SUMMARY_MAX_AGE_MS = 5 * 60 * 1000;
43
+
44
+ const CONFIRMATION_SUMMARY_LOG_INTERVAL = 25;
45
+
46
+ const confirmationSummaryTelemetry: ConfirmationSummaryTelemetry = {
47
+ ensureCalls: 0,
48
+ caseCacheHits: 0,
49
+ caseMisses: 0,
50
+ forceRefreshCalls: 0,
51
+ staleCaseRefreshes: 0,
52
+ staleFileRefreshes: 0,
53
+ missingFileRefreshes: 0,
54
+ removedFileEntries: 0,
55
+ refreshedFileEntries: 0,
56
+ summaryWrites: 0
57
+ };
58
+
59
+ export function getConfirmationSummaryTelemetry(): ConfirmationSummaryTelemetry {
60
+ return { ...confirmationSummaryTelemetry };
61
+ }
62
+
63
+ export function resetConfirmationSummaryTelemetry(): void {
64
+ for (const key of Object.keys(confirmationSummaryTelemetry) as Array<keyof ConfirmationSummaryTelemetry>) {
65
+ confirmationSummaryTelemetry[key] = 0;
66
+ }
67
+ }
68
+
69
+ function getGlobalDebugFlag(): boolean {
70
+ const globalScope = globalThis as unknown as {
71
+ __STRIAE_DEBUG_CONFIRMATION_CACHE__?: boolean;
72
+ };
73
+
74
+ return globalScope.__STRIAE_DEBUG_CONFIRMATION_CACHE__ === true;
75
+ }
76
+
77
+ function getLocalStorageDebugFlag(): boolean {
78
+ if (typeof window === 'undefined' || !window.localStorage) {
79
+ return false;
80
+ }
81
+
82
+ try {
83
+ return window.localStorage.getItem('striae.debug.confirmationCache') === 'true';
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ function shouldLogConfirmationSummaryTelemetry(): boolean {
90
+ return getGlobalDebugFlag() || getLocalStorageDebugFlag();
91
+ }
92
+
93
+ function maybeLogConfirmationSummaryTelemetrySnapshot(): void {
94
+ if (!shouldLogConfirmationSummaryTelemetry()) {
95
+ return;
96
+ }
97
+
98
+ if (
99
+ confirmationSummaryTelemetry.ensureCalls === 0 ||
100
+ confirmationSummaryTelemetry.ensureCalls % CONFIRMATION_SUMMARY_LOG_INTERVAL !== 0
101
+ ) {
102
+ return;
103
+ }
104
+
105
+ const totalCaseLookups =
106
+ confirmationSummaryTelemetry.caseCacheHits + confirmationSummaryTelemetry.caseMisses;
107
+ const caseCacheHitRate =
108
+ totalCaseLookups > 0
109
+ ? Math.round((confirmationSummaryTelemetry.caseCacheHits / totalCaseLookups) * 100)
110
+ : 0;
111
+
112
+ console.info('[confirmation-cache] summary', {
113
+ ensureCalls: confirmationSummaryTelemetry.ensureCalls,
114
+ caseCacheHitRate,
115
+ caseCacheHits: confirmationSummaryTelemetry.caseCacheHits,
116
+ caseMisses: confirmationSummaryTelemetry.caseMisses,
117
+ forceRefreshCalls: confirmationSummaryTelemetry.forceRefreshCalls,
118
+ staleCaseRefreshes: confirmationSummaryTelemetry.staleCaseRefreshes,
119
+ missingFileRefreshes: confirmationSummaryTelemetry.missingFileRefreshes,
120
+ staleFileRefreshes: confirmationSummaryTelemetry.staleFileRefreshes,
121
+ refreshedFileEntries: confirmationSummaryTelemetry.refreshedFileEntries,
122
+ removedFileEntries: confirmationSummaryTelemetry.removedFileEntries,
123
+ summaryWrites: confirmationSummaryTelemetry.summaryWrites
124
+ });
125
+ }
126
+
127
+ export function trackEnsureCall(): void {
128
+ confirmationSummaryTelemetry.ensureCalls += 1;
129
+ maybeLogConfirmationSummaryTelemetrySnapshot();
130
+ }
131
+
132
+ export function trackCaseMiss(): void {
133
+ confirmationSummaryTelemetry.caseMisses += 1;
134
+ }
135
+
136
+ export function trackCaseHit(): void {
137
+ confirmationSummaryTelemetry.caseCacheHits += 1;
138
+ }
139
+
140
+ export function trackForceRefreshCall(): void {
141
+ confirmationSummaryTelemetry.forceRefreshCalls += 1;
142
+ }
143
+
144
+ export function trackStaleCaseRefresh(): void {
145
+ confirmationSummaryTelemetry.staleCaseRefreshes += 1;
146
+ }
147
+
148
+ export function trackMissingFileRefresh(): void {
149
+ confirmationSummaryTelemetry.missingFileRefreshes += 1;
150
+ }
151
+
152
+ export function trackStaleFileRefresh(): void {
153
+ confirmationSummaryTelemetry.staleFileRefreshes += 1;
154
+ }
155
+
156
+ export function trackRemovedFileEntry(): void {
157
+ confirmationSummaryTelemetry.removedFileEntries += 1;
158
+ }
159
+
160
+ export function trackRefreshedFileEntry(): void {
161
+ confirmationSummaryTelemetry.refreshedFileEntries += 1;
162
+ }
163
+
164
+ export function trackSummaryWrite(): void {
165
+ confirmationSummaryTelemetry.summaryWrites += 1;
166
+ }
167
+
168
+ export function getIsoNow(): string {
169
+ return new Date().toISOString();
170
+ }
171
+
172
+ export function createEmptyConfirmationSummary(): UserConfirmationSummaryDocument {
173
+ return {
174
+ version: CONFIRMATION_SUMMARY_VERSION,
175
+ updatedAt: getIsoNow(),
176
+ cases: {}
177
+ };
178
+ }
179
+
180
+ export function buildConfirmationSummaryPath(user: User): string {
181
+ return `/${encodeURIComponent(user.uid)}/meta/confirmation-status.json`;
182
+ }
183
+
184
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
185
+ return !!value && typeof value === 'object' && !Array.isArray(value);
186
+ }
187
+
188
+ function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSummary {
189
+ if (!isPlainObject(value)) {
190
+ return {
191
+ includeConfirmation: false,
192
+ isConfirmed: false,
193
+ updatedAt: getIsoNow()
194
+ };
195
+ }
196
+
197
+ return {
198
+ includeConfirmation: value.includeConfirmation === true,
199
+ isConfirmed: value.isConfirmed === true,
200
+ updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.length > 0 ? value.updatedAt : getIsoNow()
201
+ };
202
+ }
203
+
204
+ export function isStaleTimestamp(timestamp: string, maxAgeMs: number): boolean {
205
+ const parsed = Date.parse(timestamp);
206
+ if (Number.isNaN(parsed)) {
207
+ return true;
208
+ }
209
+
210
+ return Date.now() - parsed > maxAgeMs;
211
+ }
212
+
213
+ export function computeCaseConfirmationAggregate(filesById: Record<string, FileConfirmationSummary>): {
214
+ includeConfirmation: boolean;
215
+ isConfirmed: boolean;
216
+ } {
217
+ const statuses = Object.values(filesById);
218
+ const filesRequiringConfirmation = statuses.filter((entry) => entry.includeConfirmation);
219
+ const includeConfirmation = filesRequiringConfirmation.length > 0;
220
+ const isConfirmed = includeConfirmation ? filesRequiringConfirmation.every((entry) => entry.isConfirmed) : false;
221
+
222
+ return {
223
+ includeConfirmation,
224
+ isConfirmed
225
+ };
226
+ }
227
+
228
+ export function toFileConfirmationSummary(annotationData: AnnotationData | null): FileConfirmationSummary {
229
+ const includeConfirmation = annotationData?.includeConfirmation === true;
230
+
231
+ return {
232
+ includeConfirmation,
233
+ isConfirmed: includeConfirmation && !!annotationData?.confirmationData,
234
+ updatedAt: getIsoNow()
235
+ };
236
+ }
237
+
238
+ export function normalizeConfirmationSummaryDocument(payload: unknown): UserConfirmationSummaryDocument {
239
+ if (!isPlainObject(payload) || !isPlainObject(payload.cases)) {
240
+ return createEmptyConfirmationSummary();
241
+ }
242
+
243
+ const normalizedCases: Record<string, CaseConfirmationSummary> = {};
244
+
245
+ for (const [caseNumber, rawCaseEntry] of Object.entries(payload.cases)) {
246
+ if (!isPlainObject(rawCaseEntry) || !isPlainObject(rawCaseEntry.filesById)) {
247
+ continue;
248
+ }
249
+
250
+ const filesById: Record<string, FileConfirmationSummary> = {};
251
+ for (const [fileId, rawFileEntry] of Object.entries(rawCaseEntry.filesById)) {
252
+ filesById[fileId] = normalizeFileConfirmationSummary(rawFileEntry);
253
+ }
254
+
255
+ const aggregate = computeCaseConfirmationAggregate(filesById);
256
+
257
+ normalizedCases[caseNumber] = {
258
+ includeConfirmation: aggregate.includeConfirmation,
259
+ isConfirmed: aggregate.isConfirmed,
260
+ updatedAt:
261
+ typeof rawCaseEntry.updatedAt === 'string' && rawCaseEntry.updatedAt.length > 0
262
+ ? rawCaseEntry.updatedAt
263
+ : getIsoNow(),
264
+ filesById
265
+ };
266
+ }
267
+
268
+ return {
269
+ version:
270
+ typeof payload.version === 'number' && Number.isFinite(payload.version)
271
+ ? payload.version
272
+ : CONFIRMATION_SUMMARY_VERSION,
273
+ updatedAt:
274
+ typeof payload.updatedAt === 'string' && payload.updatedAt.length > 0
275
+ ? payload.updatedAt
276
+ : getIsoNow(),
277
+ cases: normalizedCases
278
+ };
279
+ }