@striae-org/striae 4.2.1 → 4.3.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/app/components/actions/case-import/confirmation-import.ts +20 -1
- package/app/components/actions/case-import/orchestrator.ts +3 -0
- package/app/components/actions/case-manage.ts +5 -1
- package/app/components/actions/confirm-export.ts +12 -3
- package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
- package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
- package/app/components/canvas/canvas.tsx +2 -1
- 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.module.css +11 -0
- package/app/components/navbar/navbar.tsx +38 -19
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +690 -110
- package/app/components/sidebar/cases/cases.module.css +23 -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 +452 -145
- 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-editor-form.tsx +43 -5
- package/app/components/sidebar/notes/notes.module.css +236 -4
- 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 +8 -1
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/routes/striae/striae.tsx +45 -1
- package/app/services/audit/audit-export-csv.ts +4 -2
- package/app/services/audit/audit-export-report.ts +36 -4
- package/app/services/audit/audit.service.ts +2 -0
- package/app/services/audit/builders/audit-entry-builder.ts +1 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
- 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 +18 -2
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/forensics/confirmation-signature.ts +20 -5
- package/functions/api/image/[[path]].ts +4 -0
- package/package.json +3 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +5 -0
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
- package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +20 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- 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
|
@@ -123,8 +123,12 @@ function normalizeConfirmations(confirmations: ConfirmationMap): ConfirmationMap
|
|
|
123
123
|
|
|
124
124
|
export function createConfirmationSigningPayload(
|
|
125
125
|
confirmationData: ConfirmationImportData,
|
|
126
|
-
signatureVersion: string = CONFIRMATION_SIGNATURE_VERSION
|
|
126
|
+
signatureVersion: string = CONFIRMATION_SIGNATURE_VERSION,
|
|
127
|
+
options: {
|
|
128
|
+
includeExportedByBadgeId?: boolean;
|
|
129
|
+
} = {}
|
|
127
130
|
): string {
|
|
131
|
+
const includeExportedByBadgeId = options.includeExportedByBadgeId !== false;
|
|
128
132
|
const canonicalPayload = {
|
|
129
133
|
signatureVersion,
|
|
130
134
|
metadata: {
|
|
@@ -134,7 +138,7 @@ export function createConfirmationSigningPayload(
|
|
|
134
138
|
exportedByUid: confirmationData.metadata.exportedByUid,
|
|
135
139
|
exportedByName: confirmationData.metadata.exportedByName,
|
|
136
140
|
exportedByCompany: confirmationData.metadata.exportedByCompany,
|
|
137
|
-
...(confirmationData.metadata.exportedByBadgeId
|
|
141
|
+
...(includeExportedByBadgeId && confirmationData.metadata.exportedByBadgeId
|
|
138
142
|
? { exportedByBadgeId: confirmationData.metadata.exportedByBadgeId }
|
|
139
143
|
: {}),
|
|
140
144
|
totalConfirmations: confirmationData.metadata.totalConfirmations,
|
|
@@ -180,9 +184,7 @@ export async function verifyConfirmationSignature(
|
|
|
180
184
|
};
|
|
181
185
|
}
|
|
182
186
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
return verifySignaturePayload(
|
|
187
|
+
const verifyPayload = (payload: string) => verifySignaturePayload(
|
|
186
188
|
payload,
|
|
187
189
|
signature,
|
|
188
190
|
FORENSIC_MANIFEST_SIGNATURE_ALGORITHM,
|
|
@@ -197,4 +199,17 @@ export async function verifyConfirmationSignature(
|
|
|
197
199
|
verificationPublicKeyPem
|
|
198
200
|
}
|
|
199
201
|
);
|
|
202
|
+
|
|
203
|
+
const primaryPayload = createConfirmationSigningPayload(confirmationData, signatureVersion);
|
|
204
|
+
const primaryResult = await verifyPayload(primaryPayload);
|
|
205
|
+
|
|
206
|
+
if (primaryResult.isValid || !confirmationData.metadata.exportedByBadgeId) {
|
|
207
|
+
return primaryResult;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const legacyPayload = createConfirmationSigningPayload(confirmationData, signatureVersion, {
|
|
211
|
+
includeExportedByBadgeId: false
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return verifyPayload(legacyPayload);
|
|
200
215
|
}
|
|
@@ -53,6 +53,10 @@ function extractProxyPath(url: URL): ProxyPathResult {
|
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
const decodedPath = decodeURIComponent(encodedPath);
|
|
56
|
+
if (decodedPath.includes('?') || decodedPath.includes('#')) {
|
|
57
|
+
return { ok: false, reason: 'bad-encoding' };
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
return { ok: true, path: decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}` };
|
|
57
61
|
} catch {
|
|
58
62
|
return { ok: false, reason: 'bad-encoding' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,10 +54,9 @@
|
|
|
54
54
|
"workers/*/src/*.example.ts",
|
|
55
55
|
"workers/*/src/*.example.js",
|
|
56
56
|
"workers/*/src/*.ts",
|
|
57
|
-
"workers/pdf-worker/scripts/*.js",
|
|
58
|
-
"workers/pdf-worker/src/assets/icon-256.png",
|
|
57
|
+
"workers/pdf-worker/scripts/*.js",
|
|
59
58
|
"!workers/*/src/*worker.ts",
|
|
60
|
-
"workers/pdf-worker/src/assets/generated-assets.ts",
|
|
59
|
+
"workers/pdf-worker/src/assets/generated-assets.example.ts",
|
|
61
60
|
"workers/pdf-worker/src/formats/format-striae.ts",
|
|
62
61
|
"workers/pdf-worker/src/report-types.ts",
|
|
63
62
|
"workers/*/wrangler.jsonc.example",
|
|
@@ -13,6 +13,7 @@ export interface ConfirmationSignatureMetadata {
|
|
|
13
13
|
exportedByUid: string;
|
|
14
14
|
exportedByName: string;
|
|
15
15
|
exportedByCompany: string;
|
|
16
|
+
exportedByBadgeId?: string;
|
|
16
17
|
totalConfirmations: number;
|
|
17
18
|
version: string;
|
|
18
19
|
hash: string;
|
|
@@ -131,6 +132,7 @@ export function isValidConfirmationPayload(
|
|
|
131
132
|
typeof metadata.exportedByUid !== 'string' ||
|
|
132
133
|
typeof metadata.exportedByName !== 'string' ||
|
|
133
134
|
typeof metadata.exportedByCompany !== 'string' ||
|
|
135
|
+
(typeof metadata.exportedByBadgeId !== 'undefined' && typeof metadata.exportedByBadgeId !== 'string') ||
|
|
134
136
|
typeof metadata.totalConfirmations !== 'number' ||
|
|
135
137
|
metadata.totalConfirmations < 0 ||
|
|
136
138
|
typeof metadata.version !== 'string' ||
|
|
@@ -261,6 +263,9 @@ export function createConfirmationSigningPayload(confirmationData: ConfirmationS
|
|
|
261
263
|
exportedByUid: confirmationData.metadata.exportedByUid,
|
|
262
264
|
exportedByName: confirmationData.metadata.exportedByName,
|
|
263
265
|
exportedByCompany: confirmationData.metadata.exportedByCompany,
|
|
266
|
+
...(confirmationData.metadata.exportedByBadgeId
|
|
267
|
+
? { exportedByBadgeId: confirmationData.metadata.exportedByBadgeId }
|
|
268
|
+
: {}),
|
|
264
269
|
totalConfirmations: confirmationData.metadata.totalConfirmations,
|
|
265
270
|
version: confirmationData.metadata.version,
|
|
266
271
|
hash: confirmationData.metadata.hash.toUpperCase(),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { PDFGenerationData, ReportRenderer } from '../report-types';
|
|
1
|
+
import type { PDFGenerationData, ReportPdfOptionsBuilder, ReportRenderer } from '../report-types';
|
|
2
2
|
import { ICON_256 } from '../assets/generated-assets';
|
|
3
|
+
import { buildRepeatedChromePdfOptions, escapeHtml } from '../report-layout';
|
|
3
4
|
|
|
4
5
|
export const renderReport: ReportRenderer = (data: PDFGenerationData): string => {
|
|
5
|
-
const { imageUrl,
|
|
6
|
+
const { imageUrl, annotationData, activeAnnotations } = data;
|
|
6
7
|
const annotationsSet = new Set(activeAnnotations);
|
|
8
|
+
const hasImage = Boolean(imageUrl && imageUrl !== '/clear.jpg');
|
|
9
|
+
const safeText = (value: unknown): string => escapeHtml(String(value ?? ''));
|
|
7
10
|
|
|
8
11
|
// Programmatically determine if a color is dark and needs a light background
|
|
9
12
|
const needsLightBackground = (color: string | undefined): boolean => {
|
|
@@ -71,42 +74,23 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
71
74
|
<style>
|
|
72
75
|
html, body {
|
|
73
76
|
width: 100%;
|
|
74
|
-
height: 100%;
|
|
75
77
|
margin: 0;
|
|
76
78
|
font-family: Arial, sans-serif;
|
|
77
79
|
background-color: white;
|
|
78
|
-
display: flex;
|
|
79
|
-
flex-direction: column;
|
|
80
|
-
min-height: 100vh;
|
|
81
80
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
align-items: center;
|
|
85
|
-
margin-bottom: 15px;
|
|
86
|
-
border-bottom: 2px solid #333;
|
|
87
|
-
padding-bottom: 8px;
|
|
88
|
-
position: relative;
|
|
89
|
-
}
|
|
90
|
-
.header-content {
|
|
91
|
-
flex: 1;
|
|
92
|
-
display: flex;
|
|
93
|
-
align-items: center;
|
|
94
|
-
justify-content: space-between;
|
|
81
|
+
body {
|
|
82
|
+
color: #333;
|
|
95
83
|
}
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
.report-body {
|
|
85
|
+
width: 100%;
|
|
86
|
+
box-sizing: border-box;
|
|
99
87
|
}
|
|
100
|
-
.case-number {
|
|
101
|
-
font-size: 16px;
|
|
102
|
-
font-weight: bold;
|
|
103
|
-
color: #333;
|
|
104
|
-
text-align: right;
|
|
105
|
-
}
|
|
106
88
|
.image-container {
|
|
107
89
|
width: 100%;
|
|
108
|
-
margin:
|
|
90
|
+
margin: 0 0 10px;
|
|
109
91
|
position: relative;
|
|
92
|
+
page-break-inside: avoid;
|
|
93
|
+
break-inside: avoid;
|
|
110
94
|
}
|
|
111
95
|
.image-wrapper {
|
|
112
96
|
display: flex;
|
|
@@ -170,6 +154,8 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
170
154
|
width: 100%;
|
|
171
155
|
margin-top: 12px;
|
|
172
156
|
min-height: 50px;
|
|
157
|
+
page-break-inside: avoid;
|
|
158
|
+
break-inside: avoid;
|
|
173
159
|
}
|
|
174
160
|
.support-level-annotation {
|
|
175
161
|
flex: 1;
|
|
@@ -235,10 +221,21 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
235
221
|
}
|
|
236
222
|
.confirmation-section {
|
|
237
223
|
margin-top: 20px;
|
|
224
|
+
display: flex;
|
|
225
|
+
flex-direction: column;
|
|
226
|
+
gap: 16px;
|
|
227
|
+
}
|
|
228
|
+
.notes-page {
|
|
229
|
+
page-break-before: always;
|
|
230
|
+
break-before: page;
|
|
231
|
+
}
|
|
232
|
+
.confirmation-summary {
|
|
238
233
|
display: flex;
|
|
239
234
|
justify-content: flex-start;
|
|
240
235
|
align-items: flex-start;
|
|
241
236
|
gap: 20px;
|
|
237
|
+
page-break-inside: avoid;
|
|
238
|
+
break-inside: avoid;
|
|
242
239
|
}
|
|
243
240
|
.confirmation-box {
|
|
244
241
|
background: #ffffff;
|
|
@@ -247,6 +244,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
247
244
|
padding: 15px;
|
|
248
245
|
width: 280px;
|
|
249
246
|
font-family: 'Inter', Arial, sans-serif;
|
|
247
|
+
box-sizing: border-box;
|
|
250
248
|
}
|
|
251
249
|
.confirmation-label {
|
|
252
250
|
font-size: 14px;
|
|
@@ -274,6 +272,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
274
272
|
padding: 15px;
|
|
275
273
|
width: 280px;
|
|
276
274
|
font-family: 'Inter', Arial, sans-serif;
|
|
275
|
+
box-sizing: border-box;
|
|
277
276
|
}
|
|
278
277
|
.confirmation-title {
|
|
279
278
|
font-size: 14px;
|
|
@@ -316,64 +315,32 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
316
315
|
letter-spacing: 1px;
|
|
317
316
|
}
|
|
318
317
|
.additional-notes-section {
|
|
319
|
-
|
|
318
|
+
border: 1px solid #d7dbe0;
|
|
319
|
+
border-radius: 8px;
|
|
320
|
+
background: #fafbfc;
|
|
321
|
+
padding: 16px 18px;
|
|
322
|
+
box-sizing: border-box;
|
|
320
323
|
font-family: 'Inter', Arial, sans-serif;
|
|
321
|
-
font-size: 14px;
|
|
322
|
-
line-height: 1.6;
|
|
323
324
|
color: #333;
|
|
324
|
-
white-space: pre-wrap;
|
|
325
|
-
word-wrap: break-word;
|
|
326
|
-
text-indent: 0 !important;
|
|
327
|
-
padding: 0;
|
|
328
|
-
margin: 0;
|
|
329
|
-
margin-left: 20px;
|
|
330
|
-
flex-shrink: 0;
|
|
331
|
-
text-align: left;
|
|
332
|
-
display: block;
|
|
333
325
|
}
|
|
334
|
-
.
|
|
335
|
-
margin
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
align-items: center;
|
|
341
|
-
font-family: 'Inter', Arial, sans-serif;
|
|
342
|
-
font-size: 11px;
|
|
326
|
+
.additional-notes-title {
|
|
327
|
+
margin: 0 0 10px;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
font-weight: 700;
|
|
330
|
+
letter-spacing: 0.08em;
|
|
331
|
+
text-transform: uppercase;
|
|
343
332
|
color: #666;
|
|
344
333
|
}
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
font-weight: 500;
|
|
356
|
-
flex: 1;
|
|
357
|
-
text-align: left;
|
|
358
|
-
display: flex;
|
|
359
|
-
align-items: center;
|
|
360
|
-
gap: 6px;
|
|
361
|
-
}
|
|
362
|
-
.footer-brand-icon {
|
|
363
|
-
width: 14px;
|
|
364
|
-
height: 14px;
|
|
365
|
-
object-fit: contain;
|
|
366
|
-
}
|
|
367
|
-
.footer-center {
|
|
368
|
-
font-weight: 600;
|
|
369
|
-
flex: 1;
|
|
370
|
-
text-align: center;
|
|
371
|
-
color: #333;
|
|
372
|
-
}
|
|
373
|
-
.footer-right {
|
|
374
|
-
font-style: italic;
|
|
375
|
-
flex: 1;
|
|
376
|
-
text-align: right;
|
|
334
|
+
.additional-notes-body {
|
|
335
|
+
margin: 0;
|
|
336
|
+
font-size: 14px;
|
|
337
|
+
line-height: 1.6;
|
|
338
|
+
white-space: pre-wrap;
|
|
339
|
+
overflow-wrap: anywhere;
|
|
340
|
+
word-break: break-word;
|
|
341
|
+
text-indent: 0 !important;
|
|
342
|
+
orphans: 3;
|
|
343
|
+
widows: 3;
|
|
377
344
|
}
|
|
378
345
|
.index-section {
|
|
379
346
|
text-align: center;
|
|
@@ -395,19 +362,12 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
395
362
|
</style>
|
|
396
363
|
</head>
|
|
397
364
|
<body>
|
|
398
|
-
<div class="
|
|
399
|
-
<div class="content-wrapper">
|
|
400
|
-
<div class="header">
|
|
401
|
-
<div class="header-content">
|
|
402
|
-
<div class="date">${currentDate}</div>
|
|
403
|
-
${caseNumber ? `<div class="case-number">${caseNumber}</div>` : '<div class="case-number"></div>'}
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
365
|
+
<div class="report-body">
|
|
406
366
|
|
|
407
|
-
${
|
|
367
|
+
${hasImage ? `
|
|
408
368
|
${annotationData && annotationsSet?.has('index') && annotationData.indexType === 'number' && annotationData.indexNumber ? `
|
|
409
369
|
<div class="index-section">
|
|
410
|
-
Index: ${annotationData.indexNumber}
|
|
370
|
+
Index: ${safeText(annotationData.indexNumber)}
|
|
411
371
|
</div>
|
|
412
372
|
` : ''}
|
|
413
373
|
|
|
@@ -419,12 +379,12 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
419
379
|
<div class="annotations-overlay">
|
|
420
380
|
<div class="left-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
|
|
421
381
|
<div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
|
|
422
|
-
${annotationData.leftCase}${annotationData.leftItem ? ` ${annotationData.leftItem}` : ''}
|
|
382
|
+
${safeText(annotationData.leftCase)}${annotationData.leftItem ? ` ${safeText(annotationData.leftItem)}` : ''}
|
|
423
383
|
</div>
|
|
424
384
|
</div>
|
|
425
385
|
<div class="right-annotation" style="${needsLightBackground(annotationData.caseFontColor || '#FFDE21') ? 'background: rgba(255, 255, 255, 0.9); border: 2px solid rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);' : ''}">
|
|
426
386
|
<div class="case-text" style="color: ${annotationData.caseFontColor || '#FFDE21'};">
|
|
427
|
-
${annotationData.rightCase}${annotationData.rightItem ? ` ${annotationData.rightItem}` : ''}
|
|
387
|
+
${safeText(annotationData.rightCase)}${annotationData.rightItem ? ` ${safeText(annotationData.rightItem)}` : ''}
|
|
428
388
|
</div>
|
|
429
389
|
</div>
|
|
430
390
|
</div>
|
|
@@ -450,7 +410,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
450
410
|
${annotationData && annotationsSet?.has('id') ? `
|
|
451
411
|
<div class="support-level-annotation">
|
|
452
412
|
<div class="support-level-text" style="color: ${annotationData.supportLevel === 'ID' ? '#28a745' : annotationData.supportLevel === 'Exclusion' ? '#dc3545' : '#ffc107'}; background: ${annotationData.supportLevel === 'Inconclusive' ? 'rgba(120, 120, 120, 0.95)' : 'rgba(240, 240, 240, 0.95)'};">
|
|
453
|
-
${annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel}
|
|
413
|
+
${safeText(annotationData.supportLevel === 'ID' ? 'Identification' : annotationData.supportLevel)}
|
|
454
414
|
</div>
|
|
455
415
|
</div>
|
|
456
416
|
` : '<div class="support-level-annotation"></div>'}
|
|
@@ -458,7 +418,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
458
418
|
${annotationData && annotationsSet?.has('class') ? `
|
|
459
419
|
<div class="class-annotation">
|
|
460
420
|
<div class="class-text-annotation">
|
|
461
|
-
${annotationData.customClass || annotationData.classType}${annotationData.classNote ? ` (${annotationData.classNote})` : ''}
|
|
421
|
+
${safeText(annotationData.customClass || annotationData.classType)}${annotationData.classNote ? ` (${safeText(annotationData.classNote)})` : ''}
|
|
462
422
|
</div>
|
|
463
423
|
</div>
|
|
464
424
|
` : '<div class="class-annotation"></div>'}
|
|
@@ -477,20 +437,21 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
477
437
|
${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
|
|
478
438
|
<div class="confirmation-section">
|
|
479
439
|
${annotationData && (annotationData.includeConfirmation === true) ? `
|
|
440
|
+
<div class="confirmation-summary">
|
|
480
441
|
${annotationData.confirmationData ? `
|
|
481
442
|
<div class="confirmation-data">
|
|
482
443
|
<div class="confirmation-title">IDENTIFICATION CONFIRMED</div>
|
|
483
444
|
<div class="confirmation-field">
|
|
484
|
-
<div class="confirmation-name">${annotationData.confirmationData.fullName}, ${annotationData.confirmationData.badgeId}</div>
|
|
445
|
+
<div class="confirmation-name">${safeText(annotationData.confirmationData.fullName)}, ${safeText(annotationData.confirmationData.badgeId)}</div>
|
|
485
446
|
</div>
|
|
486
447
|
<div class="confirmation-field">
|
|
487
|
-
<div class="confirmation-company">${annotationData.confirmationData.confirmedByCompany || 'N/A'}</div>
|
|
448
|
+
<div class="confirmation-company">${safeText(annotationData.confirmationData.confirmedByCompany || 'N/A')}</div>
|
|
488
449
|
</div>
|
|
489
450
|
<div class="confirmation-field">
|
|
490
|
-
<div class="confirmation-timestamp">${annotationData.confirmationData.timestamp}</div>
|
|
451
|
+
<div class="confirmation-timestamp">${safeText(annotationData.confirmationData.timestamp)}</div>
|
|
491
452
|
</div>
|
|
492
453
|
<div class="confirmation-field">
|
|
493
|
-
<div class="confirmation-id">ID: ${annotationData.confirmationData.confirmationId}</div>
|
|
454
|
+
<div class="confirmation-id">ID: ${safeText(annotationData.confirmationData.confirmationId)}</div>
|
|
494
455
|
</div>
|
|
495
456
|
</div>
|
|
496
457
|
` : `
|
|
@@ -501,30 +462,35 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
|
|
|
501
462
|
<div class="confirmation-line"></div>
|
|
502
463
|
</div>
|
|
503
464
|
`}
|
|
504
|
-
|
|
465
|
+
</div>
|
|
466
|
+
` : ''}
|
|
505
467
|
|
|
506
468
|
${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
|
|
507
|
-
<
|
|
508
|
-
|
|
469
|
+
<section class="additional-notes-section ${hasImage || annotationData.includeConfirmation === true ? 'notes-page' : ''}">
|
|
470
|
+
<h2 class="additional-notes-title">Additional Notes</h2>
|
|
471
|
+
<p class="additional-notes-body">${escapeHtml(annotationData.additionalNotes.trim())}</p>
|
|
472
|
+
</section>
|
|
473
|
+
` : ''}
|
|
509
474
|
</div>
|
|
510
475
|
` : ''}
|
|
511
476
|
|
|
512
477
|
</div>
|
|
513
|
-
</div>
|
|
514
|
-
|
|
515
|
-
<div class="footer">
|
|
516
|
-
<div class="footer-left">
|
|
517
|
-
<span>Notes formatted by Striae</span>
|
|
518
|
-
<img class="footer-brand-icon" src="${ICON_256}" alt="Striae icon" />
|
|
519
|
-
</div>
|
|
520
|
-
<div class="footer-center">
|
|
521
|
-
${userCompany ? userCompany : ''}
|
|
522
|
-
</div>
|
|
523
|
-
<div class="footer-right">
|
|
524
|
-
${notesUpdatedFormatted ? `Notes updated ${notesUpdatedFormatted}` : ''}
|
|
525
|
-
</div>
|
|
526
|
-
</div>
|
|
527
478
|
</body>
|
|
528
479
|
</html>
|
|
529
480
|
`;
|
|
530
|
-
};
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const getPdfOptions: ReportPdfOptionsBuilder = (data: PDFGenerationData) => buildRepeatedChromePdfOptions({
|
|
484
|
+
headerLeft: data.currentDate,
|
|
485
|
+
headerRight: data.caseNumber,
|
|
486
|
+
headerDetailLeft: [data.annotationData?.leftCase, data.annotationData?.leftItem].filter(Boolean).join(' / ')
|
|
487
|
+
? `Left Case / Item: ${[data.annotationData?.leftCase, data.annotationData?.leftItem].filter(Boolean).join(' / ')}`
|
|
488
|
+
: undefined,
|
|
489
|
+
headerDetailRight: [data.annotationData?.rightCase, data.annotationData?.rightItem].filter(Boolean).join(' / ')
|
|
490
|
+
? `Right Case / Item: ${[data.annotationData?.rightCase, data.annotationData?.rightItem].filter(Boolean).join(' / ')}`
|
|
491
|
+
: undefined,
|
|
492
|
+
footerLeft: 'Notes formatted by Striae',
|
|
493
|
+
footerCenter: data.userCompany,
|
|
494
|
+
footerRight: data.notesUpdatedFormatted ? `Notes updated ${data.notesUpdatedFormatted}` : undefined,
|
|
495
|
+
footerLeftImageSrc: ICON_256,
|
|
496
|
+
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import type { PDFGenerationData, PDFGenerationRequest, ReportModule } from './report-types';
|
|
1
|
+
import type { PDFGenerationData, PDFGenerationRequest, ReportModule, ReportPdfOptions } from './report-types';
|
|
2
2
|
|
|
3
3
|
interface Env {
|
|
4
4
|
BROWSER: Fetcher;
|
|
5
5
|
PDF_WORKER_AUTH: string;
|
|
6
|
-
ACCOUNT_ID
|
|
7
|
-
BROWSER_API_TOKEN
|
|
6
|
+
ACCOUNT_ID?: string;
|
|
7
|
+
BROWSER_API_TOKEN?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const BROWSER_PDF_TIMEOUT_MS = 90_000;
|
|
11
11
|
const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
|
|
12
12
|
|
|
13
|
-
const DEFAULT_PDF_OPTIONS = {
|
|
13
|
+
const DEFAULT_PDF_OPTIONS: ReportPdfOptions = {
|
|
14
14
|
printBackground: true,
|
|
15
15
|
format: 'letter',
|
|
16
16
|
margin: {
|
|
@@ -21,6 +21,17 @@ const DEFAULT_PDF_OPTIONS = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
function resolvePdfOptions(overrides?: Partial<ReportPdfOptions>): ReportPdfOptions {
|
|
25
|
+
return {
|
|
26
|
+
...DEFAULT_PDF_OPTIONS,
|
|
27
|
+
...overrides,
|
|
28
|
+
margin: {
|
|
29
|
+
...DEFAULT_PDF_OPTIONS.margin,
|
|
30
|
+
...overrides?.margin,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
|
|
25
36
|
// Default Striae report format module
|
|
26
37
|
striae: () => import('./formats/format-striae'),
|
|
@@ -63,7 +74,11 @@ class MissingSecretError extends Error {
|
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
|
|
66
|
-
function getRequiredSecret(value: string, name: string): string {
|
|
77
|
+
function getRequiredSecret(value: string | undefined, name: string): string {
|
|
78
|
+
if (typeof value !== 'string') {
|
|
79
|
+
throw new MissingSecretError(name);
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
const normalized = value.trim();
|
|
68
83
|
if (!normalized) {
|
|
69
84
|
throw new MissingSecretError(name);
|
|
@@ -97,7 +112,7 @@ function resolveReportRequest(payload: unknown): PDFGenerationRequest {
|
|
|
97
112
|
};
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<string> {
|
|
115
|
+
async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<{ html: string; pdfOptions: ReportPdfOptions }> {
|
|
101
116
|
const loader = reportModuleLoaders[reportFormat];
|
|
102
117
|
|
|
103
118
|
if (!loader) {
|
|
@@ -106,17 +121,20 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
|
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
const reportModule = await loader();
|
|
109
|
-
return
|
|
124
|
+
return {
|
|
125
|
+
html: reportModule.renderReport(data),
|
|
126
|
+
pdfOptions: resolvePdfOptions(reportModule.getPdfOptions?.(data)),
|
|
127
|
+
};
|
|
110
128
|
}
|
|
111
129
|
|
|
112
|
-
async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
|
|
130
|
+
async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: ReportPdfOptions): Promise<Response> {
|
|
113
131
|
const accountId = getRequiredSecret(env.ACCOUNT_ID, 'ACCOUNT_ID');
|
|
114
132
|
const browserApiToken = getRequiredSecret(env.BROWSER_API_TOKEN, 'BROWSER_API_TOKEN');
|
|
115
133
|
|
|
116
134
|
const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
|
|
117
135
|
const requestBody = JSON.stringify({
|
|
118
136
|
html,
|
|
119
|
-
pdfOptions
|
|
137
|
+
pdfOptions,
|
|
120
138
|
});
|
|
121
139
|
|
|
122
140
|
let endpointResponse: Response;
|
|
@@ -192,7 +210,7 @@ export default {
|
|
|
192
210
|
const { reportFormat, data } = resolveReportRequest(payload);
|
|
193
211
|
const document = await renderReport(reportFormat, data);
|
|
194
212
|
|
|
195
|
-
return await renderPdfViaRestEndpoint(env, document);
|
|
213
|
+
return await renderPdfViaRestEndpoint(env, document.html, document.pdfOptions);
|
|
196
214
|
} catch (error) {
|
|
197
215
|
if (error instanceof MissingSecretError) {
|
|
198
216
|
console.error(`[pdf-worker] Configuration error: ${error.message}`);
|