@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
@@ -1,19 +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
6
  ACCOUNT_ID?: string;
7
- CLOUDFLARE_ACCOUNT_ID?: string;
8
7
  BROWSER_API_TOKEN?: string;
9
- API_TOKEN?: string;
10
8
  }
11
9
 
12
- const DEFAULT_REPORT_FORMAT = 'striae';
13
10
  const BROWSER_PDF_TIMEOUT_MS = 90_000;
14
11
  const BROWSER_RENDERING_API_BASE = 'https://api.cloudflare.com/client/v4/accounts';
15
12
 
16
- const DEFAULT_PDF_OPTIONS = {
13
+ const DEFAULT_PDF_OPTIONS: ReportPdfOptions = {
17
14
  printBackground: true,
18
15
  format: 'letter',
19
16
  margin: {
@@ -24,6 +21,17 @@ const DEFAULT_PDF_OPTIONS = {
24
21
  },
25
22
  };
26
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
+
27
35
  const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
28
36
  // Default Striae report format module
29
37
  striae: () => import('./formats/format-striae'),
@@ -56,66 +64,55 @@ function jsonResponse(body: unknown, status: number): Response {
56
64
  });
57
65
  }
58
66
 
59
- function resolveBrowserApiToken(env: Env): string {
60
- const candidates = [env.BROWSER_API_TOKEN, env.API_TOKEN];
67
+ class MissingSecretError extends Error {
68
+ readonly secretKey: string;
61
69
 
62
- for (const candidate of candidates) {
63
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
64
- return candidate.trim();
65
- }
70
+ constructor(key: string) {
71
+ super(`Worker is missing required secret: ${key}`);
72
+ this.name = 'MissingSecretError';
73
+ this.secretKey = key;
66
74
  }
67
-
68
- return '';
69
75
  }
70
76
 
71
- function resolveAccountId(env: Env): string {
72
- const candidates = [env.ACCOUNT_ID, env.CLOUDFLARE_ACCOUNT_ID];
73
-
74
- for (const candidate of candidates) {
75
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
76
- return candidate.trim();
77
- }
77
+ function getRequiredSecret(value: string | undefined, name: string): string {
78
+ if (typeof value !== 'string') {
79
+ throw new MissingSecretError(name);
78
80
  }
79
81
 
80
- return '';
81
- }
82
-
83
- function normalizeReportFormat(format: unknown): string {
84
- if (typeof format !== 'string') {
85
- return DEFAULT_REPORT_FORMAT;
82
+ const normalized = value.trim();
83
+ if (!normalized) {
84
+ throw new MissingSecretError(name);
86
85
  }
87
86
 
88
- const normalized = format.trim().toLowerCase();
89
- return normalized || DEFAULT_REPORT_FORMAT;
87
+ return normalized;
90
88
  }
91
89
 
92
- function resolveReportRequest(payload: unknown): { reportFormat: string; data: PDFGenerationData } {
93
- if (!payload || typeof payload !== 'object') {
90
+ function resolveReportRequest(payload: unknown): PDFGenerationRequest {
91
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
94
92
  throw new Error('Request body must be a JSON object');
95
93
  }
96
94
 
97
95
  const record = payload as Record<string, unknown>;
98
- const reportFormat = normalizeReportFormat(record.reportFormat);
96
+ if (typeof record.reportFormat !== 'string' || record.reportFormat.trim().length === 0) {
97
+ throw new Error('Request body must include a non-empty reportFormat');
98
+ }
99
99
 
100
- if (record.data && typeof record.data === 'object') {
101
- return {
102
- reportFormat,
103
- data: record.data as PDFGenerationData,
104
- };
100
+ if (!record.data || typeof record.data !== 'object' || Array.isArray(record.data)) {
101
+ throw new Error('Request body must include a data object');
105
102
  }
106
103
 
107
- // Backward compatibility: accept legacy top-level payload shape.
108
- const legacyData: Record<string, unknown> = { ...record };
109
- delete legacyData.reportFormat;
110
- delete legacyData.data;
104
+ const data = record.data as Record<string, unknown>;
105
+ if (typeof data.currentDate !== 'string' || data.currentDate.trim().length === 0) {
106
+ throw new Error('Request body data must include a non-empty currentDate');
107
+ }
111
108
 
112
109
  return {
113
- reportFormat,
114
- data: legacyData as PDFGenerationData,
110
+ reportFormat: record.reportFormat.trim().toLowerCase(),
111
+ data: record.data as PDFGenerationData,
115
112
  };
116
113
  }
117
114
 
118
- async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<string> {
115
+ async function renderReport(reportFormat: string, data: PDFGenerationData): Promise<{ html: string; pdfOptions: ReportPdfOptions }> {
119
116
  const loader = reportModuleLoaders[reportFormat];
120
117
 
121
118
  if (!loader) {
@@ -124,28 +121,20 @@ async function renderReport(reportFormat: string, data: PDFGenerationData): Prom
124
121
  }
125
122
 
126
123
  const reportModule = await loader();
127
- return reportModule.renderReport(data);
124
+ return {
125
+ html: reportModule.renderReport(data),
126
+ pdfOptions: resolvePdfOptions(reportModule.getPdfOptions?.(data)),
127
+ };
128
128
  }
129
129
 
130
- async function renderPdfViaRestEndpoint(env: Env, html: string): Promise<Response> {
131
- const accountId = resolveAccountId(env);
132
- const browserApiToken = resolveBrowserApiToken(env);
133
-
134
- if (!accountId || !browserApiToken) {
135
- return jsonResponse(
136
- {
137
- error: 'Missing required Browser Rendering credentials',
138
- requiredSecrets: ['ACCOUNT_ID', 'BROWSER_API_TOKEN'],
139
- note: 'Set ACCOUNT_ID and a Browser Rendering - Edit token (BROWSER_API_TOKEN) on this worker.',
140
- },
141
- 502
142
- );
143
- }
130
+ async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: ReportPdfOptions): Promise<Response> {
131
+ const accountId = getRequiredSecret(env.ACCOUNT_ID, 'ACCOUNT_ID');
132
+ const browserApiToken = getRequiredSecret(env.BROWSER_API_TOKEN, 'BROWSER_API_TOKEN');
144
133
 
145
134
  const endpoint = `${BROWSER_RENDERING_API_BASE}/${accountId}/browser-rendering/pdf`;
146
135
  const requestBody = JSON.stringify({
147
136
  html,
148
- pdfOptions: DEFAULT_PDF_OPTIONS,
137
+ pdfOptions,
149
138
  });
150
139
 
151
140
  let endpointResponse: Response;
@@ -217,12 +206,20 @@ export default {
217
206
 
218
207
  if (request.method === 'POST') {
219
208
  try {
220
- const payload = await request.json() as PDFGenerationData | PDFGenerationRequest;
209
+ const payload = await request.json() as unknown;
221
210
  const { reportFormat, data } = resolveReportRequest(payload);
222
211
  const document = await renderReport(reportFormat, data);
223
212
 
224
- return await renderPdfViaRestEndpoint(env, document);
213
+ return await renderPdfViaRestEndpoint(env, document.html, document.pdfOptions);
225
214
  } catch (error) {
215
+ if (error instanceof MissingSecretError) {
216
+ console.error(`[pdf-worker] Configuration error: ${error.message}`);
217
+ return jsonResponse(
218
+ { error: 'Worker configuration error', missing_secret: error.secretKey },
219
+ 502
220
+ );
221
+ }
222
+
226
223
  if (isTimeoutError(error)) {
227
224
  const timeoutMessage = error instanceof Error ? error.message : 'PDF generation timed out';
228
225
  return jsonResponse({ error: timeoutMessage }, 504);
@@ -0,0 +1,227 @@
1
+ import type { ReportPdfOptions } from './report-types';
2
+
3
+ interface ReportChromeTemplateConfig {
4
+ headerLeft?: string;
5
+ headerCenter?: string;
6
+ headerRight?: string;
7
+ headerDetailLeft?: string;
8
+ headerDetailRight?: string;
9
+ footerLeft?: string;
10
+ footerCenter?: string;
11
+ footerRight?: string;
12
+ footerLeftImageSrc?: string;
13
+ includePageNumbers?: boolean;
14
+ }
15
+
16
+ const HEADER_TEMPLATE_STYLES = `
17
+ <style>
18
+ .report-header {
19
+ width: 100%;
20
+ box-sizing: border-box;
21
+ padding: 0 0.5in 8px;
22
+ border-bottom: 2px solid #333333;
23
+ color: #333333;
24
+ font-family: Arial, sans-serif;
25
+ font-size: 18px;
26
+ font-weight: 700;
27
+ }
28
+ .report-header__content {
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: space-between;
32
+ gap: 12px;
33
+ width: 100%;
34
+ }
35
+ .report-header__details {
36
+ display: flex;
37
+ align-items: flex-start;
38
+ justify-content: space-between;
39
+ gap: 12px;
40
+ width: 100%;
41
+ margin-top: 8px;
42
+ padding-top: 8px;
43
+ border-top: 1px solid #d9d9d9;
44
+ font-size: 10px;
45
+ font-weight: 600;
46
+ letter-spacing: 0.04em;
47
+ text-transform: uppercase;
48
+ color: #666666;
49
+ }
50
+ .report-header__cell {
51
+ flex: 1 1 0;
52
+ min-width: 0;
53
+ white-space: nowrap;
54
+ overflow: hidden;
55
+ text-overflow: ellipsis;
56
+ }
57
+ .report-header__cell--left {
58
+ text-align: left;
59
+ }
60
+ .report-header__cell--center {
61
+ text-align: center;
62
+ }
63
+ .report-header__cell--right {
64
+ text-align: right;
65
+ }
66
+ .report-header__detail {
67
+ flex: 1 1 0;
68
+ min-width: 0;
69
+ white-space: nowrap;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ }
73
+ .report-header__detail--left {
74
+ text-align: left;
75
+ }
76
+ .report-header__detail--right {
77
+ text-align: right;
78
+ }
79
+ </style>
80
+ `;
81
+
82
+ const FOOTER_TEMPLATE_STYLES = `
83
+ <style>
84
+ .report-footer {
85
+ width: 100%;
86
+ box-sizing: border-box;
87
+ padding: 8px 0.5in 0;
88
+ border-top: 1px solid #cccccc;
89
+ color: #666666;
90
+ font-family: Arial, sans-serif;
91
+ font-size: 9px;
92
+ }
93
+ .report-footer__content {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: space-between;
97
+ gap: 12px;
98
+ width: 100%;
99
+ }
100
+ .report-footer__cell {
101
+ flex: 1 1 0;
102
+ min-width: 0;
103
+ white-space: nowrap;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ }
107
+ .report-footer__cell--left {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 6px;
111
+ text-align: left;
112
+ font-weight: 500;
113
+ }
114
+ .report-footer__cell--center {
115
+ text-align: center;
116
+ color: #333333;
117
+ font-weight: 600;
118
+ }
119
+ .report-footer__cell--right {
120
+ text-align: right;
121
+ font-style: italic;
122
+ }
123
+ .report-footer__page-count {
124
+ font-style: normal;
125
+ font-weight: 600;
126
+ color: #333333;
127
+ }
128
+ .report-footer__separator {
129
+ margin: 0 6px;
130
+ color: #999999;
131
+ font-style: normal;
132
+ }
133
+ .report-footer__icon {
134
+ width: 12px;
135
+ height: 12px;
136
+ object-fit: contain;
137
+ flex: 0 0 auto;
138
+ }
139
+ </style>
140
+ `;
141
+
142
+ export function escapeHtml(value: string | undefined): string {
143
+ if (!value) {
144
+ return '';
145
+ }
146
+
147
+ return value
148
+ .replace(/&/g, '&amp;')
149
+ .replace(/</g, '&lt;')
150
+ .replace(/>/g, '&gt;')
151
+ .replace(/"/g, '&quot;')
152
+ .replace(/'/g, '&#39;');
153
+ }
154
+
155
+ function renderTemplateCell(value: string | undefined, className: string): string {
156
+ const content = value && value.trim().length > 0 ? escapeHtml(value.trim()) : '&nbsp;';
157
+ return `<div class="${className}">${content}</div>`;
158
+ }
159
+
160
+ export function buildRepeatedChromePdfOptions(config: ReportChromeTemplateConfig): Partial<ReportPdfOptions> {
161
+ const hasHeaderDetails = Boolean(
162
+ (config.headerDetailLeft && config.headerDetailLeft.trim().length > 0) ||
163
+ (config.headerDetailRight && config.headerDetailRight.trim().length > 0)
164
+ );
165
+
166
+ const headerDetails = hasHeaderDetails
167
+ ? `
168
+ <div class="report-header__details">
169
+ ${renderTemplateCell(config.headerDetailLeft, 'report-header__detail report-header__detail--left')}
170
+ ${renderTemplateCell(config.headerDetailRight, 'report-header__detail report-header__detail--right')}
171
+ </div>
172
+ `
173
+ : '';
174
+
175
+ const headerTemplate = `
176
+ ${HEADER_TEMPLATE_STYLES}
177
+ <div class="report-header">
178
+ <div class="report-header__content">
179
+ ${renderTemplateCell(config.headerLeft, 'report-header__cell report-header__cell--left')}
180
+ ${renderTemplateCell(config.headerCenter, 'report-header__cell report-header__cell--center')}
181
+ ${renderTemplateCell(config.headerRight, 'report-header__cell report-header__cell--right')}
182
+ </div>
183
+ ${headerDetails}
184
+ </div>
185
+ `;
186
+
187
+ const footerLeftContent = config.footerLeft && config.footerLeft.trim().length > 0
188
+ ? `<span>${escapeHtml(config.footerLeft.trim())}</span>`
189
+ : '<span>&nbsp;</span>';
190
+
191
+ const footerIcon = config.footerLeftImageSrc
192
+ ? `<img class="report-footer__icon" src="${escapeHtml(config.footerLeftImageSrc)}" alt="" />`
193
+ : '';
194
+
195
+ const footerRightText = config.footerRight && config.footerRight.trim().length > 0
196
+ ? `<span>${escapeHtml(config.footerRight.trim())}</span>`
197
+ : '';
198
+
199
+ const footerPageCount = config.includePageNumbers === false
200
+ ? ''
201
+ : `<span class="report-footer__page-count">Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>`;
202
+
203
+ const footerRightContent = footerRightText && footerPageCount
204
+ ? `${footerRightText}<span class="report-footer__separator">|</span>${footerPageCount}`
205
+ : footerRightText || footerPageCount || '&nbsp;';
206
+
207
+ const footerTemplate = `
208
+ ${FOOTER_TEMPLATE_STYLES}
209
+ <div class="report-footer">
210
+ <div class="report-footer__content">
211
+ <div class="report-footer__cell report-footer__cell--left">${footerLeftContent}${footerIcon}</div>
212
+ ${renderTemplateCell(config.footerCenter, 'report-footer__cell report-footer__cell--center')}
213
+ <div class="report-footer__cell report-footer__cell--right">${footerRightContent}</div>
214
+ </div>
215
+ </div>
216
+ `;
217
+
218
+ return {
219
+ displayHeaderFooter: true,
220
+ headerTemplate,
221
+ footerTemplate,
222
+ margin: {
223
+ top: hasHeaderDetails ? '1.45in' : '1.15in',
224
+ bottom: '0.8in',
225
+ },
226
+ };
227
+ }
@@ -52,7 +52,7 @@ export interface PDFGenerationData {
52
52
  caseNumber?: string;
53
53
  annotationData?: AnnotationData;
54
54
  activeAnnotations?: string[];
55
- currentDate?: string;
55
+ currentDate: string;
56
56
  notesUpdatedFormatted?: string;
57
57
  userCompany?: string;
58
58
  userFirstName?: string;
@@ -61,12 +61,32 @@ export interface PDFGenerationData {
61
61
  }
62
62
 
63
63
  export interface PDFGenerationRequest {
64
- reportFormat?: string;
65
- data?: PDFGenerationData;
64
+ reportFormat: string;
65
+ data: PDFGenerationData;
66
+ }
67
+
68
+ export interface PDFMarginOptions {
69
+ top: string;
70
+ bottom: string;
71
+ left: string;
72
+ right: string;
73
+ }
74
+
75
+ export interface ReportPdfOptions {
76
+ printBackground?: boolean;
77
+ format?: string;
78
+ margin?: Partial<PDFMarginOptions>;
79
+ displayHeaderFooter?: boolean;
80
+ headerTemplate?: string;
81
+ footerTemplate?: string;
82
+ preferCSSPageSize?: boolean;
66
83
  }
67
84
 
68
85
  export type ReportRenderer = (data: PDFGenerationData) => string;
69
86
 
87
+ export type ReportPdfOptionsBuilder = (data: PDFGenerationData) => Partial<ReportPdfOptions>;
88
+
70
89
  export interface ReportModule {
71
90
  renderReport: ReportRenderer;
91
+ getPdfOptions?: ReportPdfOptionsBuilder;
72
92
  }
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-20",
5
+ "compatibility_date": "2026-03-22",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -12,7 +12,7 @@
12
12
  "@cloudflare/puppeteer": "^1.0.6",
13
13
  "@cloudflare/vitest-pool-workers": "^0.13.0",
14
14
  "vitest": "~4.1.0",
15
- "wrangler": "^4.73.0"
15
+ "wrangler": "^4.76.0"
16
16
  },
17
17
  "overrides": {
18
18
  "undici": "7.24.1",
@@ -433,6 +433,22 @@ async function deleteSingleCase(env: Env, userUid: string, caseNumber: string):
433
433
  }
434
434
  }
435
435
 
436
+ async function deleteUserConfirmationSummary(env: Env, userUid: string): Promise<void> {
437
+ const dataApiKey = env.R2_KEY_SECRET;
438
+ const dataWorkerBaseUrl = resolveDataWorkerBaseUrl(env);
439
+ const encodedUserId = encodeURIComponent(userUid);
440
+ const confirmationSummaryPath = `${dataWorkerBaseUrl}/${encodedUserId}/meta/confirmation-status.json`;
441
+
442
+ const response = await fetch(confirmationSummaryPath, {
443
+ method: 'DELETE',
444
+ headers: { 'X-Custom-Auth-Key': dataApiKey }
445
+ });
446
+
447
+ if (!response.ok && response.status !== 404) {
448
+ throw new Error(`Failed to delete confirmation summary metadata: ${response.status}`);
449
+ }
450
+ }
451
+
436
452
  async function executeUserDeletion(
437
453
  env: Env,
438
454
  userUid: string,
@@ -490,6 +506,7 @@ async function executeUserDeletion(
490
506
  throw new Error(`Failed to fully delete all case data: ${caseCleanupErrors.join(' | ')}`);
491
507
  }
492
508
 
509
+ await deleteUserConfirmationSummary(env, userUid);
493
510
  await deleteFirebaseAuthUser(env, userUid);
494
511
 
495
512
  // Delete the user account from the database
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-03-20",
5
+ "compatibility_date": "2026-03-22",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-03-20"
3
+ compatibility_date = "2026-03-22"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6
 
package/NOTICE DELETED
@@ -1,13 +0,0 @@
1
- NOTICE
2
-
3
- Striae – A Firearms Examiner’s Comparison Companion
4
-
5
- Striae © 2025. All rights reserved.
6
-
7
- Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License in the repository’s LICENSE file.
8
-
9
- Attributions for bundled components:
10
-
11
- Portions of this product may include open-source software licensed under their respective licenses, including but not limited to: Remix, Cloudflare Workers, Firebase, Tailwind CSS, Vite, and other npm packages. License texts for third-party components are provided in their respective source files or in node_modules as applicable.
12
-
13
- No trademark rights are granted for “Striae” or any associated names or logos.
@@ -1,52 +0,0 @@
1
- import { useState } from 'react';
2
- import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
- import styles from './notes.module.css';
4
-
5
- interface NotesModalProps {
6
- isOpen: boolean;
7
- onClose: () => void;
8
- notes: string;
9
- onSave: (notes: string) => void;
10
- }
11
-
12
- export const NotesModal = ({ isOpen, onClose, notes, onSave }: NotesModalProps) => {
13
- const [tempNotes, setTempNotes] = useState(notes);
14
- const {
15
- requestClose,
16
- overlayProps,
17
- getCloseButtonProps
18
- } = useOverlayDismiss({
19
- isOpen,
20
- onClose
21
- });
22
-
23
- if (!isOpen) return null;
24
-
25
- const handleSave = () => {
26
- onSave(tempNotes);
27
- requestClose();
28
- };
29
-
30
- return (
31
- <div
32
- className={styles.modalOverlay}
33
- aria-label="Close notes dialog"
34
- {...overlayProps}
35
- >
36
- <div className={styles.modal}>
37
- <button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
38
- <h5 className={styles.modalTitle}>Additional Notes</h5>
39
- <textarea
40
- value={tempNotes}
41
- onChange={(e) => setTempNotes(e.target.value)}
42
- className={styles.modalTextarea}
43
- placeholder="Enter additional notes..."
44
- />
45
- <div className={styles.modalButtons}>
46
- <button onClick={handleSave} className={styles.saveButton}>Save</button>
47
- <button onClick={requestClose} className={styles.cancelButton}>Cancel</button>
48
- </div>
49
- </div>
50
- </div>
51
- );
52
- };
package/postcss.config.js DELETED
@@ -1,6 +0,0 @@
1
- export default {
2
- plugins: {
3
- tailwindcss: {},
4
- autoprefixer: {},
5
- },
6
- };
@@ -1,22 +0,0 @@
1
- import type { Config } from "tailwindcss";
2
-
3
- export default {
4
- content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
5
- theme: {
6
- extend: {
7
- fontFamily: {
8
- sans: [
9
- "Inter",
10
- "ui-sans-serif",
11
- "system-ui",
12
- "sans-serif",
13
- "Apple Color Emoji",
14
- "Segoe UI Emoji",
15
- "Segoe UI Symbol",
16
- "Noto Color Emoji",
17
- ],
18
- },
19
- },
20
- },
21
- plugins: [],
22
- } satisfies Config;