@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.
Files changed (90) 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/case-modals/archive-case-modal.module.css +0 -76
  9. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  10. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  11. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  13. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  14. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  15. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  16. package/app/components/navbar/navbar.tsx +34 -9
  17. package/app/components/sidebar/cases/case-sidebar.tsx +93 -73
  18. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  19. package/app/components/sidebar/cases/cases-modal.tsx +737 -116
  20. package/app/components/sidebar/cases/cases.module.css +43 -0
  21. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  22. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  23. package/app/components/sidebar/files/files-modal.module.css +285 -44
  24. package/app/components/sidebar/files/files-modal.tsx +482 -177
  25. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  26. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  27. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  28. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  29. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  30. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +77 -76
  31. package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
  32. package/app/components/sidebar/notes/notes.module.css +262 -14
  33. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  34. package/app/components/sidebar/sidebar-container.tsx +2 -0
  35. package/app/components/sidebar/sidebar.tsx +15 -1
  36. package/app/{tailwind.css → global.css} +1 -3
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/hooks/useOverlayDismiss.ts +6 -4
  40. package/app/root.tsx +1 -1
  41. package/app/routes/striae/striae.tsx +7 -0
  42. package/app/services/audit/audit.service.ts +2 -2
  43. package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
  44. package/app/types/annotations.ts +48 -1
  45. package/app/types/audit.ts +1 -0
  46. package/app/utils/data/case-filters.ts +127 -0
  47. package/app/utils/data/confirmation-summary/summary-core.ts +295 -0
  48. package/app/utils/data/data-operations.ts +17 -861
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/data/index.ts +11 -1
  51. package/app/utils/data/operations/batch-operations.ts +113 -0
  52. package/app/utils/data/operations/case-operations.ts +168 -0
  53. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  54. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  55. package/app/utils/data/operations/index.ts +7 -0
  56. package/app/utils/data/operations/signing-operations.ts +225 -0
  57. package/app/utils/data/operations/types.ts +42 -0
  58. package/app/utils/data/operations/validation-operations.ts +48 -0
  59. package/app/utils/forensics/export-verification.ts +40 -111
  60. package/functions/api/_shared/firebase-auth.ts +2 -7
  61. package/functions/api/image/[[path]].ts +23 -22
  62. package/functions/api/pdf/[[path]].ts +27 -8
  63. package/package.json +7 -13
  64. package/scripts/deploy-primershear-emails.sh +1 -1
  65. package/worker-configuration.d.ts +2 -2
  66. package/workers/audit-worker/package.json +1 -1
  67. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  68. package/workers/data-worker/package.json +1 -1
  69. package/workers/data-worker/wrangler.jsonc.example +1 -1
  70. package/workers/image-worker/package.json +1 -1
  71. package/workers/image-worker/src/image-worker.example.ts +16 -5
  72. package/workers/image-worker/wrangler.jsonc.example +1 -1
  73. package/workers/keys-worker/package.json +1 -1
  74. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  75. package/workers/pdf-worker/package.json +1 -1
  76. package/workers/pdf-worker/src/formats/format-striae.ts +84 -124
  77. package/workers/pdf-worker/src/pdf-worker.example.ts +58 -61
  78. package/workers/pdf-worker/src/report-layout.ts +227 -0
  79. package/workers/pdf-worker/src/report-types.ts +23 -3
  80. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  81. package/workers/user-worker/package.json +1 -1
  82. package/workers/user-worker/src/user-worker.example.ts +17 -0
  83. package/workers/user-worker/wrangler.jsonc.example +1 -1
  84. package/wrangler.toml.example +1 -1
  85. package/NOTICE +0 -13
  86. package/app/components/sidebar/notes/notes-modal.tsx +0 -52
  87. package/postcss.config.js +0 -6
  88. package/tailwind.config.ts +0 -22
  89. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  90. /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
+ }