@striae-org/striae 4.2.0 → 4.3.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.
- 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/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +737 -116
- package/app/components/sidebar/cases/cases.module.css +43 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +482 -177
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +262 -14
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +2 -0
- package/app/components/sidebar/sidebar.tsx +15 -1
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +7 -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/annotations.ts +48 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/file-filters.ts +201 -0
- 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 +23 -22
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +7 -13
- 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 +84 -124
- package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +23 -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
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
classType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CaseConfirmationSummary {
|
|
12
|
+
includeConfirmation: boolean;
|
|
13
|
+
isConfirmed: boolean;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
filesById: Record<string, FileConfirmationSummary>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UserConfirmationSummaryDocument {
|
|
19
|
+
version: number;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
cases: Record<string, CaseConfirmationSummary>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConfirmationSummaryEnsureOptions {
|
|
25
|
+
forceRefresh?: boolean;
|
|
26
|
+
maxAgeMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ConfirmationSummaryTelemetry {
|
|
30
|
+
ensureCalls: number;
|
|
31
|
+
caseCacheHits: number;
|
|
32
|
+
caseMisses: number;
|
|
33
|
+
forceRefreshCalls: number;
|
|
34
|
+
staleCaseRefreshes: number;
|
|
35
|
+
staleFileRefreshes: number;
|
|
36
|
+
missingFileRefreshes: number;
|
|
37
|
+
removedFileEntries: number;
|
|
38
|
+
refreshedFileEntries: number;
|
|
39
|
+
summaryWrites: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const CONFIRMATION_SUMMARY_VERSION = 1;
|
|
43
|
+
export const DEFAULT_CONFIRMATION_SUMMARY_MAX_AGE_MS = 5 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
const CONFIRMATION_SUMMARY_LOG_INTERVAL = 25;
|
|
46
|
+
|
|
47
|
+
const confirmationSummaryTelemetry: ConfirmationSummaryTelemetry = {
|
|
48
|
+
ensureCalls: 0,
|
|
49
|
+
caseCacheHits: 0,
|
|
50
|
+
caseMisses: 0,
|
|
51
|
+
forceRefreshCalls: 0,
|
|
52
|
+
staleCaseRefreshes: 0,
|
|
53
|
+
staleFileRefreshes: 0,
|
|
54
|
+
missingFileRefreshes: 0,
|
|
55
|
+
removedFileEntries: 0,
|
|
56
|
+
refreshedFileEntries: 0,
|
|
57
|
+
summaryWrites: 0
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function getConfirmationSummaryTelemetry(): ConfirmationSummaryTelemetry {
|
|
61
|
+
return { ...confirmationSummaryTelemetry };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resetConfirmationSummaryTelemetry(): void {
|
|
65
|
+
for (const key of Object.keys(confirmationSummaryTelemetry) as Array<keyof ConfirmationSummaryTelemetry>) {
|
|
66
|
+
confirmationSummaryTelemetry[key] = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getGlobalDebugFlag(): boolean {
|
|
71
|
+
const globalScope = globalThis as unknown as {
|
|
72
|
+
__STRIAE_DEBUG_CONFIRMATION_CACHE__?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return globalScope.__STRIAE_DEBUG_CONFIRMATION_CACHE__ === true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getLocalStorageDebugFlag(): boolean {
|
|
79
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return window.localStorage.getItem('striae.debug.confirmationCache') === 'true';
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function shouldLogConfirmationSummaryTelemetry(): boolean {
|
|
91
|
+
return getGlobalDebugFlag() || getLocalStorageDebugFlag();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function maybeLogConfirmationSummaryTelemetrySnapshot(): void {
|
|
95
|
+
if (!shouldLogConfirmationSummaryTelemetry()) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
confirmationSummaryTelemetry.ensureCalls === 0 ||
|
|
101
|
+
confirmationSummaryTelemetry.ensureCalls % CONFIRMATION_SUMMARY_LOG_INTERVAL !== 0
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const totalCaseLookups =
|
|
107
|
+
confirmationSummaryTelemetry.caseCacheHits + confirmationSummaryTelemetry.caseMisses;
|
|
108
|
+
const caseCacheHitRate =
|
|
109
|
+
totalCaseLookups > 0
|
|
110
|
+
? Math.round((confirmationSummaryTelemetry.caseCacheHits / totalCaseLookups) * 100)
|
|
111
|
+
: 0;
|
|
112
|
+
|
|
113
|
+
console.info('[confirmation-cache] summary', {
|
|
114
|
+
ensureCalls: confirmationSummaryTelemetry.ensureCalls,
|
|
115
|
+
caseCacheHitRate,
|
|
116
|
+
caseCacheHits: confirmationSummaryTelemetry.caseCacheHits,
|
|
117
|
+
caseMisses: confirmationSummaryTelemetry.caseMisses,
|
|
118
|
+
forceRefreshCalls: confirmationSummaryTelemetry.forceRefreshCalls,
|
|
119
|
+
staleCaseRefreshes: confirmationSummaryTelemetry.staleCaseRefreshes,
|
|
120
|
+
missingFileRefreshes: confirmationSummaryTelemetry.missingFileRefreshes,
|
|
121
|
+
staleFileRefreshes: confirmationSummaryTelemetry.staleFileRefreshes,
|
|
122
|
+
refreshedFileEntries: confirmationSummaryTelemetry.refreshedFileEntries,
|
|
123
|
+
removedFileEntries: confirmationSummaryTelemetry.removedFileEntries,
|
|
124
|
+
summaryWrites: confirmationSummaryTelemetry.summaryWrites
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function trackEnsureCall(): void {
|
|
129
|
+
confirmationSummaryTelemetry.ensureCalls += 1;
|
|
130
|
+
maybeLogConfirmationSummaryTelemetrySnapshot();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function trackCaseMiss(): void {
|
|
134
|
+
confirmationSummaryTelemetry.caseMisses += 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function trackCaseHit(): void {
|
|
138
|
+
confirmationSummaryTelemetry.caseCacheHits += 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function trackForceRefreshCall(): void {
|
|
142
|
+
confirmationSummaryTelemetry.forceRefreshCalls += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function trackStaleCaseRefresh(): void {
|
|
146
|
+
confirmationSummaryTelemetry.staleCaseRefreshes += 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function trackMissingFileRefresh(): void {
|
|
150
|
+
confirmationSummaryTelemetry.missingFileRefreshes += 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function trackStaleFileRefresh(): void {
|
|
154
|
+
confirmationSummaryTelemetry.staleFileRefreshes += 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function trackRemovedFileEntry(): void {
|
|
158
|
+
confirmationSummaryTelemetry.removedFileEntries += 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function trackRefreshedFileEntry(): void {
|
|
162
|
+
confirmationSummaryTelemetry.refreshedFileEntries += 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function trackSummaryWrite(): void {
|
|
166
|
+
confirmationSummaryTelemetry.summaryWrites += 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getIsoNow(): string {
|
|
170
|
+
return new Date().toISOString();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createEmptyConfirmationSummary(): UserConfirmationSummaryDocument {
|
|
174
|
+
return {
|
|
175
|
+
version: CONFIRMATION_SUMMARY_VERSION,
|
|
176
|
+
updatedAt: getIsoNow(),
|
|
177
|
+
cases: {}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildConfirmationSummaryPath(user: User): string {
|
|
182
|
+
return `/${encodeURIComponent(user.uid)}/meta/confirmation-status.json`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
186
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSummary {
|
|
190
|
+
if (!isPlainObject(value)) {
|
|
191
|
+
return {
|
|
192
|
+
includeConfirmation: false,
|
|
193
|
+
isConfirmed: false,
|
|
194
|
+
updatedAt: getIsoNow()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const classType = value.classType;
|
|
199
|
+
const normalizedClassType = typeof classType === 'string' && ['Bullet', 'Cartridge Case', 'Shotshell', 'Other'].includes(classType) ? (classType as 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other') : undefined;
|
|
200
|
+
|
|
201
|
+
const summary: FileConfirmationSummary = {
|
|
202
|
+
includeConfirmation: value.includeConfirmation === true,
|
|
203
|
+
isConfirmed: value.isConfirmed === true,
|
|
204
|
+
updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.length > 0 ? value.updatedAt : getIsoNow()
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (normalizedClassType) {
|
|
208
|
+
summary.classType = normalizedClassType;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return summary;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function isStaleTimestamp(timestamp: string, maxAgeMs: number): boolean {
|
|
215
|
+
const parsed = Date.parse(timestamp);
|
|
216
|
+
if (Number.isNaN(parsed)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Date.now() - parsed > maxAgeMs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function computeCaseConfirmationAggregate(filesById: Record<string, FileConfirmationSummary>): {
|
|
224
|
+
includeConfirmation: boolean;
|
|
225
|
+
isConfirmed: boolean;
|
|
226
|
+
} {
|
|
227
|
+
const statuses = Object.values(filesById);
|
|
228
|
+
const filesRequiringConfirmation = statuses.filter((entry) => entry.includeConfirmation);
|
|
229
|
+
const includeConfirmation = filesRequiringConfirmation.length > 0;
|
|
230
|
+
const isConfirmed = includeConfirmation ? filesRequiringConfirmation.every((entry) => entry.isConfirmed) : false;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
includeConfirmation,
|
|
234
|
+
isConfirmed
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function toFileConfirmationSummary(annotationData: AnnotationData | null): FileConfirmationSummary {
|
|
239
|
+
const includeConfirmation = annotationData?.includeConfirmation === true;
|
|
240
|
+
|
|
241
|
+
const summary: FileConfirmationSummary = {
|
|
242
|
+
includeConfirmation,
|
|
243
|
+
isConfirmed: includeConfirmation && !!annotationData?.confirmationData,
|
|
244
|
+
updatedAt: getIsoNow()
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (annotationData?.classType) {
|
|
248
|
+
summary.classType = annotationData.classType;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return summary;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function normalizeConfirmationSummaryDocument(payload: unknown): UserConfirmationSummaryDocument {
|
|
255
|
+
if (!isPlainObject(payload) || !isPlainObject(payload.cases)) {
|
|
256
|
+
return createEmptyConfirmationSummary();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalizedCases: Record<string, CaseConfirmationSummary> = {};
|
|
260
|
+
|
|
261
|
+
for (const [caseNumber, rawCaseEntry] of Object.entries(payload.cases)) {
|
|
262
|
+
if (!isPlainObject(rawCaseEntry) || !isPlainObject(rawCaseEntry.filesById)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const filesById: Record<string, FileConfirmationSummary> = {};
|
|
267
|
+
for (const [fileId, rawFileEntry] of Object.entries(rawCaseEntry.filesById)) {
|
|
268
|
+
filesById[fileId] = normalizeFileConfirmationSummary(rawFileEntry);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const aggregate = computeCaseConfirmationAggregate(filesById);
|
|
272
|
+
|
|
273
|
+
normalizedCases[caseNumber] = {
|
|
274
|
+
includeConfirmation: aggregate.includeConfirmation,
|
|
275
|
+
isConfirmed: aggregate.isConfirmed,
|
|
276
|
+
updatedAt:
|
|
277
|
+
typeof rawCaseEntry.updatedAt === 'string' && rawCaseEntry.updatedAt.length > 0
|
|
278
|
+
? rawCaseEntry.updatedAt
|
|
279
|
+
: getIsoNow(),
|
|
280
|
+
filesById
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
version:
|
|
286
|
+
typeof payload.version === 'number' && Number.isFinite(payload.version)
|
|
287
|
+
? payload.version
|
|
288
|
+
: CONFIRMATION_SUMMARY_VERSION,
|
|
289
|
+
updatedAt:
|
|
290
|
+
typeof payload.updatedAt === 'string' && payload.updatedAt.length > 0
|
|
291
|
+
? payload.updatedAt
|
|
292
|
+
: getIsoNow(),
|
|
293
|
+
cases: normalizedCases
|
|
294
|
+
};
|
|
295
|
+
}
|