@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.
- package/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
- package/app/components/sidebar/cases/cases-modal.tsx +76 -35
- package/app/components/sidebar/cases/cases.module.css +20 -0
- package/app/components/sidebar/files/files-modal.tsx +37 -39
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +27 -11
- package/app/components/sidebar/sidebar-container.tsx +1 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +6 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +5 -10
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- 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
|
-
|
|
121
|
+
archiveReason: input.archiveReason,
|
|
122
122
|
totalFiles: input.totalFiles,
|
|
123
123
|
lastModified: archivedAt,
|
|
124
124
|
},
|
package/app/types/audit.ts
CHANGED
|
@@ -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
|
+
}
|