@striae-org/striae 6.0.0 → 6.1.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 (46) hide show
  1. package/README.md +3 -1
  2. package/app/components/actions/case-export/core-export.ts +11 -2
  3. package/app/components/actions/case-export/download-handlers.ts +3 -1
  4. package/app/components/canvas/canvas.module.css +1 -1
  5. package/app/components/canvas/canvas.tsx +32 -11
  6. package/app/components/icon/icons.svg +1 -1
  7. package/app/components/icon/manifest.json +1 -1
  8. package/app/components/navbar/navbar.tsx +10 -9
  9. package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
  10. package/app/components/sidebar/files/files-modal.tsx +39 -15
  11. package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
  12. package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
  13. package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
  14. package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
  15. package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
  16. package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
  17. package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
  18. package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
  19. package/app/components/sidebar/notes/notes.module.css +40 -20
  20. package/app/components/sidebar/sidebar-container.tsx +8 -0
  21. package/app/components/sidebar/sidebar.tsx +3 -0
  22. package/app/components/toolbar/toolbar.tsx +5 -5
  23. package/{members.emails.example → app/config-example/members.emails} +1 -1
  24. package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
  25. package/app/hooks/useFileListPreferences.ts +22 -17
  26. package/app/routes/striae/striae.tsx +4 -10
  27. package/app/types/annotations.ts +28 -5
  28. package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
  29. package/app/utils/data/file-filters.ts +39 -17
  30. package/package.json +139 -141
  31. package/scripts/deploy-config.sh +33 -0
  32. package/scripts/deploy-members-emails.sh +4 -4
  33. package/scripts/deploy-primershear-emails.sh +3 -3
  34. package/workers/audit-worker/package.json +2 -2
  35. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  36. package/workers/data-worker/package.json +2 -2
  37. package/workers/data-worker/wrangler.jsonc.example +1 -1
  38. package/workers/image-worker/package.json +2 -2
  39. package/workers/image-worker/wrangler.jsonc.example +1 -1
  40. package/workers/pdf-worker/package.json +2 -2
  41. package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
  42. package/workers/pdf-worker/src/report-types.ts +13 -1
  43. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  44. package/workers/user-worker/package.json +2 -2
  45. package/workers/user-worker/wrangler.jsonc.example +1 -1
  46. package/wrangler.toml.example +1 -1
package/package.json CHANGED
@@ -1,142 +1,140 @@
1
- {
2
- "name": "@striae-org/striae",
3
- "version": "6.0.0",
4
- "private": false,
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
- "license": "Apache-2.0",
7
- "homepage": "https://github.com/striae-org/striae/wiki",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/striae-org/striae.git"
11
- },
12
- "funding": {
13
- "type": "github",
14
- "url": "https://github.com/sponsors/striae-org"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/striae-org/striae/issues"
18
- },
19
- "keywords": [
20
- "forensics",
21
- "firearms",
22
- "annotation",
23
- "react",
24
- "cloudflare-workers",
25
- "authenticated",
26
- "confirmations",
27
- "chain-of-custody",
28
- "audit-trail"
29
- ],
30
- "publishConfig": {
31
- "access": "public"
32
- },
33
- "files": [
34
- "app/",
35
- "!app/config",
36
- "!app/routes/auth/login.tsx",
37
- "!app/routes/auth/login.module.css",
38
- "react-router.config.ts",
39
- "load-context.ts",
40
- "scripts/",
41
- "functions/",
42
- "public/",
43
- "workers/",
44
- "!workers/*/.wrangler",
45
- "!workers/*/package-lock.json",
46
- "!workers/*/worker-configuration.d.ts",
47
- "!workers/*/wrangler.jsonc",
48
- "!workers/*/src/**/*worker.ts",
49
- "!workers/pdf-worker/src/assets/**/*",
50
- "workers/pdf-worker/src/assets/generated-assets.example.ts",
51
- "!workers/pdf-worker/src/formats/**/*",
52
- "workers/pdf-worker/src/formats/format-striae.ts",
53
- ".env.example",
54
- "primershear.emails.example",
55
- "members.emails.example",
56
- "firebase.json",
57
- "tsconfig.json",
58
- "vite.config.ts",
59
- "/worker-configuration.d.ts",
60
- "wrangler.toml.example",
61
- "LICENSE"
62
- ],
63
- "sideEffects": false,
64
- "type": "module",
65
- "scripts": {
66
- "deploy:all": "bash ./scripts/deploy-all.sh",
67
- "emulators": "firebase emulators:start --only auth",
68
- "dev": "node ./scripts/dev.cjs && react-router dev",
69
- "build": "node ./scripts/dev.cjs && react-router build",
70
- "clean": "rm -rf build node_modules/.cache .cache",
71
- "clean:build": "npm run clean && npm run build",
72
- "deploy": "npm run build && wrangler pages deploy",
73
- "publish:npm": "npm publish --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
74
- "publish:npm:dry-run": "npm publish --dry-run --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
75
- "publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
76
- "publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
77
- "publish:all": "npm run publish:npm && npm run publish:github",
78
- "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
79
- "lint": "node ./scripts/run-eslint.cjs",
80
- "start": "node ./scripts/dev.cjs && wrangler pages dev",
81
- "typecheck": "react-router typegen && tsc",
82
- "typegen": "wrangler types",
83
- "preview": "npm run build && wrangler pages dev",
84
- "cf-typegen": "wrangler types",
85
- "enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
86
- "unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
87
- "update-versions": "node ./scripts/update-markdown-versions.cjs",
88
- "update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
89
- "deploy-config": "bash ./scripts/deploy-config.sh",
90
- "update-env": "bash ./scripts/deploy-config.sh --update-env",
91
- "install-workers": "bash ./scripts/install-workers.sh",
92
- "deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
93
- "deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
94
- "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
95
- "deploy-pages": "bash ./scripts/deploy-pages.sh",
96
- "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
97
- "deploy-members": "bash ./scripts/deploy-members-emails.sh",
98
- "deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
99
- "deploy-workers:data": "cd workers/data-worker && npm run deploy",
100
- "deploy-workers:image": "cd workers/image-worker && npm run deploy",
101
- "deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
102
- "deploy-workers:user": "cd workers/user-worker && npm run deploy"
103
- },
104
- "dependencies": {
105
- "@react-router/cloudflare": "^7.14.0",
106
- "firebase": "^12.12.0",
107
- "isbot": "^5.1.37",
108
- "jszip": "^3.10.1",
109
- "qrcode": "^1.5.4",
110
- "react": "^19.2.5",
111
- "react-dom": "^19.2.5",
112
- "react-router": "^7.14.0"
113
- },
114
- "devDependencies": {
115
- "@react-router/dev": "^7.14.0",
116
- "@react-router/fs-routes": "^7.14.0",
117
- "@types/qrcode": "^1.5.6",
118
- "@types/react": "^19.2.14",
119
- "@types/react-dom": "^19.2.3",
120
- "@typescript-eslint/eslint-plugin": "^8.58.1",
121
- "@typescript-eslint/parser": "^8.58.1",
122
- "eslint": "^9.39.4",
123
- "eslint-import-resolver-typescript": "^4.4.4",
124
- "eslint-plugin-import": "^2.32.0",
125
- "eslint-plugin-jsx-a11y": "^6.10.2",
126
- "eslint-plugin-react": "^7.37.5",
127
- "eslint-plugin-react-hooks": "^7.0.1",
128
- "firebase-admin": "^13.8.0",
129
- "modern-normalize": "^3.0.1",
130
- "typescript": "^5.9.3",
131
- "vite": "^7.3.2",
132
- "vite-tsconfig-paths": "^6.1.1",
133
- "wrangler": "^4.81.1"
134
- },
135
- "overrides": {
136
- "@tootallnate/once": "3.0.1"
137
- },
138
- "engines": {
139
- "node": ">=20.19.0"
140
- },
141
- "packageManager": "npm@11.11.0"
1
+ {
2
+ "name": "@striae-org/striae",
3
+ "version": "6.1.0",
4
+ "private": false,
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
+ "license": "Apache-2.0",
7
+ "homepage": "https://github.com/striae-org/striae/wiki",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/striae-org/striae.git"
11
+ },
12
+ "funding": {
13
+ "type": "github",
14
+ "url": "https://github.com/sponsors/striae-org"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/striae-org/striae/issues"
18
+ },
19
+ "keywords": [
20
+ "forensics",
21
+ "firearms",
22
+ "annotation",
23
+ "react",
24
+ "cloudflare-workers",
25
+ "authenticated",
26
+ "confirmations",
27
+ "chain-of-custody",
28
+ "audit-trail"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "files": [
34
+ "app/",
35
+ "!app/config",
36
+ "!app/routes/auth/login.tsx",
37
+ "!app/routes/auth/login.module.css",
38
+ "react-router.config.ts",
39
+ "load-context.ts",
40
+ "scripts/",
41
+ "functions/",
42
+ "public/",
43
+ "workers/",
44
+ "!workers/*/.wrangler",
45
+ "!workers/*/package-lock.json",
46
+ "!workers/*/worker-configuration.d.ts",
47
+ "!workers/*/wrangler.jsonc",
48
+ "!workers/*/src/**/*worker.ts",
49
+ "!workers/pdf-worker/src/assets/**/*",
50
+ "workers/pdf-worker/src/assets/generated-assets.example.ts",
51
+ "!workers/pdf-worker/src/formats/**/*",
52
+ "workers/pdf-worker/src/formats/format-striae.ts",
53
+ ".env.example",
54
+ "firebase.json",
55
+ "tsconfig.json",
56
+ "vite.config.ts",
57
+ "/worker-configuration.d.ts",
58
+ "wrangler.toml.example",
59
+ "LICENSE"
60
+ ],
61
+ "sideEffects": false,
62
+ "type": "module",
63
+ "scripts": {
64
+ "deploy:all": "bash ./scripts/deploy-all.sh",
65
+ "emulators": "firebase emulators:start --only auth",
66
+ "dev": "node ./scripts/dev.cjs && react-router dev",
67
+ "build": "node ./scripts/dev.cjs && react-router build",
68
+ "clean": "rm -rf build node_modules/.cache .cache",
69
+ "clean:build": "npm run clean && npm run build",
70
+ "deploy": "npm run build && wrangler pages deploy",
71
+ "publish:npm": "npm publish --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
72
+ "publish:npm:dry-run": "npm publish --dry-run --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
73
+ "publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
74
+ "publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
75
+ "publish:all": "npm run publish:npm && npm run publish:github",
76
+ "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
77
+ "lint": "node ./scripts/run-eslint.cjs",
78
+ "start": "node ./scripts/dev.cjs && wrangler pages dev",
79
+ "typecheck": "react-router typegen && tsc",
80
+ "typegen": "wrangler types",
81
+ "preview": "npm run build && wrangler pages dev",
82
+ "cf-typegen": "wrangler types",
83
+ "enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
84
+ "unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
85
+ "update-versions": "node ./scripts/update-markdown-versions.cjs",
86
+ "update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
87
+ "deploy-config": "bash ./scripts/deploy-config.sh",
88
+ "update-env": "bash ./scripts/deploy-config.sh --update-env",
89
+ "install-workers": "bash ./scripts/install-workers.sh",
90
+ "deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
91
+ "deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
92
+ "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
93
+ "deploy-pages": "bash ./scripts/deploy-pages.sh",
94
+ "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
95
+ "deploy-members": "bash ./scripts/deploy-members-emails.sh",
96
+ "deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
97
+ "deploy-workers:data": "cd workers/data-worker && npm run deploy",
98
+ "deploy-workers:image": "cd workers/image-worker && npm run deploy",
99
+ "deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
100
+ "deploy-workers:user": "cd workers/user-worker && npm run deploy"
101
+ },
102
+ "dependencies": {
103
+ "@react-router/cloudflare": "^7.14.1",
104
+ "firebase": "^12.12.0",
105
+ "isbot": "^5.1.38",
106
+ "jszip": "^3.10.1",
107
+ "qrcode": "^1.5.4",
108
+ "react": "^19.2.5",
109
+ "react-dom": "^19.2.5",
110
+ "react-router": "^7.14.1"
111
+ },
112
+ "devDependencies": {
113
+ "@react-router/dev": "^7.14.1",
114
+ "@react-router/fs-routes": "^7.14.1",
115
+ "@types/qrcode": "^1.5.6",
116
+ "@types/react": "^19.2.14",
117
+ "@types/react-dom": "^19.2.3",
118
+ "@typescript-eslint/eslint-plugin": "^8.58.2",
119
+ "@typescript-eslint/parser": "^8.58.2",
120
+ "eslint": "^9.39.4",
121
+ "eslint-import-resolver-typescript": "^4.4.4",
122
+ "eslint-plugin-import": "^2.32.0",
123
+ "eslint-plugin-jsx-a11y": "^6.10.2",
124
+ "eslint-plugin-react": "^7.37.5",
125
+ "eslint-plugin-react-hooks": "^7.0.1",
126
+ "firebase-admin": "^13.8.0",
127
+ "modern-normalize": "^3.0.1",
128
+ "typescript": "^5.9.3",
129
+ "vite": "^7.3.2",
130
+ "vite-tsconfig-paths": "^6.1.1",
131
+ "wrangler": "^4.82.2"
132
+ },
133
+ "overrides": {
134
+ "@tootallnate/once": "3.0.1"
135
+ },
136
+ "engines": {
137
+ "node": ">=20.19.0"
138
+ },
139
+ "packageManager": "npm@11.12.0"
142
140
  }
@@ -200,6 +200,36 @@ source "$DEPLOY_CONFIG_VALIDATION_MODULE"
200
200
  source "$DEPLOY_CONFIG_SCAFFOLDING_MODULE"
201
201
  source "$DEPLOY_CONFIG_PROMPT_MODULE"
202
202
 
203
+ EMAIL_LIST_CONFIG_DIR="app/config"
204
+ MEMBERS_EMAILS_FILE="$EMAIL_LIST_CONFIG_DIR/members.emails"
205
+ PRIMERSHEAR_EMAILS_FILE="$EMAIL_LIST_CONFIG_DIR/primershear.emails"
206
+
207
+ sync_env_var_from_email_list_file() {
208
+ local env_var_name=$1
209
+ local file_path=$2
210
+ local loaded_values=""
211
+
212
+ if [ ! -f "$file_path" ]; then
213
+ echo -e "${YELLOW}⚠️ $file_path not found; keeping existing $env_var_name value in .env${NC}"
214
+ return 0
215
+ fi
216
+
217
+ loaded_values=$(grep -v '^[[:space:]]*#' "$file_path" | grep -v '^[[:space:]]*$' | sed -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | paste -sd ',' - || true)
218
+
219
+ write_env_var "$env_var_name" "$loaded_values"
220
+ export "$env_var_name=$loaded_values"
221
+
222
+ local loaded_count
223
+ loaded_count=$(echo "$loaded_values" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
224
+ echo -e "${GREEN}✅ Synced $env_var_name from $file_path ($loaded_count entry/entries)${NC}"
225
+ }
226
+
227
+ sync_email_list_env_vars_from_config() {
228
+ echo -e "${YELLOW}📧 Syncing optional email list env vars from app/config...${NC}"
229
+ sync_env_var_from_email_list_file "REGISTRATION_EMAILS" "$MEMBERS_EMAILS_FILE"
230
+ sync_env_var_from_email_list_file "PRIMERSHEAR_EMAILS" "$PRIMERSHEAR_EMAILS_FILE"
231
+ }
232
+
203
233
  if [ "$validate_only" = "true" ]; then
204
234
  echo -e "\n${BLUE}🧪 Validate-only mode enabled${NC}"
205
235
  run_validation_checkpoint
@@ -217,6 +247,9 @@ load_admin_service_credentials
217
247
  # Always prompt for secrets to ensure configuration
218
248
  prompt_for_secrets
219
249
 
250
+ # Keep optional email list env vars aligned with app/config source files.
251
+ sync_email_list_env_vars_from_config
252
+
220
253
  # Validate after secrets have been configured
221
254
  validate_required_vars
222
255
 
@@ -3,7 +3,7 @@
3
3
  # ============================================
4
4
  # MEMBERS EMAIL LIST DEPLOYMENT SCRIPT
5
5
  # ============================================
6
- # Reads members.emails, updates REGISTRATION_EMAILS in .env,
6
+ # Reads app/config/members.emails, updates REGISTRATION_EMAILS in .env,
7
7
  # then deploys that secret directly to Cloudflare Pages (production).
8
8
 
9
9
  set -e
@@ -26,12 +26,12 @@ trap 'echo -e "\n${RED}❌ deploy-members-emails.sh failed near line ${LINENO}${
26
26
 
27
27
  # ── Read emails file ──────────────────────────────────────────────────────────
28
28
 
29
- EMAILS_FILE="$PROJECT_ROOT/members.emails"
29
+ EMAILS_FILE="$PROJECT_ROOT/app/config/members.emails"
30
30
 
31
31
  if [ ! -f "$EMAILS_FILE" ]; then
32
32
  echo -e "${RED}❌ members.emails not found at: $EMAILS_FILE${NC}"
33
33
  echo -e "${YELLOW} Create it with one email address or @domain.com wildcard per line.${NC}"
34
- echo -e "${YELLOW} See members.emails.example for the format.${NC}"
34
+ echo -e "${YELLOW} See app/config-example/members.emails for the format.${NC}"
35
35
  exit 1
36
36
  fi
37
37
 
@@ -45,7 +45,7 @@ if [ -z "$REGISTRATION_EMAILS" ]; then
45
45
  fi
46
46
 
47
47
  ENTRY_COUNT=$(echo "$REGISTRATION_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
48
- echo -e "${GREEN}✅ Loaded $ENTRY_COUNT entry(ies) from members.emails${NC}"
48
+ echo -e "${GREEN}✅ Loaded $ENTRY_COUNT entry(ies) from app/config/members.emails${NC}"
49
49
 
50
50
  # ── Update .env ───────────────────────────────────────────────────────────────
51
51
 
@@ -3,7 +3,7 @@
3
3
  # ============================================
4
4
  # PRIMERSHEAR EMAIL LIST DEPLOYMENT SCRIPT
5
5
  # ============================================
6
- # Reads primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
6
+ # Reads app/config/primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
7
7
  # then deploys that secret directly to Cloudflare Pages (production).
8
8
 
9
9
  set -e
@@ -26,7 +26,7 @@ trap 'echo -e "\n${RED}❌ deploy-primershear-emails.sh failed near line ${LINEN
26
26
 
27
27
  # ── Read emails file ──────────────────────────────────────────────────────────
28
28
 
29
- EMAILS_FILE="$PROJECT_ROOT/primershear.emails"
29
+ EMAILS_FILE="$PROJECT_ROOT/app/config/primershear.emails"
30
30
 
31
31
  if [ ! -f "$EMAILS_FILE" ]; then
32
32
  echo -e "${RED}❌ primershear.emails not found at: $EMAILS_FILE${NC}"
@@ -44,7 +44,7 @@ if [ -z "$PRIMERSHEAR_EMAILS" ]; then
44
44
  fi
45
45
 
46
46
  EMAIL_COUNT=$(echo "$PRIMERSHEAR_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
47
- echo -e "${GREEN}✅ Loaded $EMAIL_COUNT email address(es) from primershear.emails${NC}"
47
+ echo -e "${GREEN}✅ Loaded $EMAIL_COUNT email address(es) from app/config/primershear.emails${NC}"
48
48
 
49
49
  # ── Update .env ───────────────────────────────────────────────────────────────
50
50
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-worker",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.81.1"
11
+ "wrangler": "^4.82.2"
12
12
  }
13
13
  }
@@ -7,7 +7,7 @@
7
7
  "name": "AUDIT_WORKER_NAME",
8
8
  "account_id": "ACCOUNT_ID",
9
9
  "main": "src/audit-worker.ts",
10
- "compatibility_date": "2026-04-11",
10
+ "compatibility_date": "2026-04-14",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-worker",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.81.1"
11
+ "wrangler": "^4.82.2"
12
12
  }
13
13
  }
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-04-11",
8
+ "compatibility_date": "2026-04-14",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-worker",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -8,6 +8,6 @@
8
8
  "start": "wrangler dev"
9
9
  },
10
10
  "devDependencies": {
11
- "wrangler": "^4.81.1"
11
+ "wrangler": "^4.82.2"
12
12
  }
13
13
  }
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-04-11",
5
+ "compatibility_date": "2026-04-14",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "generate:assets": "node scripts/generate-assets.js",
@@ -9,6 +9,6 @@
9
9
  "start": "wrangler dev"
10
10
  },
11
11
  "devDependencies": {
12
- "wrangler": "^4.81.1"
12
+ "wrangler": "^4.82.2"
13
13
  }
14
14
  }
@@ -7,6 +7,14 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
7
7
  const annotationsSet = new Set(activeAnnotations);
8
8
  const hasImage = Boolean(imageUrl && imageUrl !== '/clear.jpg');
9
9
  const safeText = (value: unknown): string => escapeHtml(String(value ?? ''));
10
+ const leftAdditionalNotes = annotationData?.leftAdditionalNotes?.trim() || '';
11
+ const rightAdditionalNotes = annotationData?.rightAdditionalNotes?.trim() || '';
12
+ const generalAdditionalNotes = annotationData?.additionalNotes?.trim() || '';
13
+ const hasSideAdditionalNotes = Boolean(leftAdditionalNotes || rightAdditionalNotes);
14
+ const hasGeneralAdditionalNotes = Boolean(generalAdditionalNotes);
15
+ const hasAdditionalNotes = hasSideAdditionalNotes || hasGeneralAdditionalNotes;
16
+ const hasConfirmationOrNotes = Boolean(annotationData && ((annotationData.includeConfirmation === true) || hasAdditionalNotes));
17
+ const notesShouldStartNewPage = hasImage || annotationData?.includeConfirmation === true;
10
18
 
11
19
  // Programmatically determine if a color is dark and needs a light background
12
20
  const needsLightBackground = (color: string | undefined): boolean => {
@@ -323,6 +331,15 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
323
331
  font-family: 'Inter', Arial, sans-serif;
324
332
  color: #333;
325
333
  }
334
+ .additional-notes-grid {
335
+ display: flex;
336
+ align-items: stretch;
337
+ gap: 12px;
338
+ }
339
+ .additional-notes-section--half {
340
+ flex: 1 1 50%;
341
+ width: 50%;
342
+ }
326
343
  .additional-notes-title {
327
344
  margin: 0 0 10px;
328
345
  font-size: 12px;
@@ -415,15 +432,31 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
415
432
  </div>
416
433
  ` : '<div class="support-level-annotation"></div>'}
417
434
 
418
- ${annotationData && annotationsSet?.has('class') ? `
435
+ ${annotationData && annotationsSet?.has('item') ? `
419
436
  <div class="class-annotation">
420
437
  <div class="class-text-annotation">
421
- ${safeText(annotationData.customClass || annotationData.classType)}${annotationData.classNote ? ` (${safeText(annotationData.classNote)})` : ''}
438
+ ${(() => {
439
+ const leftValue = annotationData.leftCustomClass || annotationData.leftItemType;
440
+ const rightValue = annotationData.rightCustomClass || annotationData.rightItemType;
441
+ const legacyValue = annotationData.customClass || annotationData.itemType || annotationData.classType;
442
+ const displayValue =
443
+ leftValue && rightValue && leftValue !== rightValue
444
+ ? `${leftValue} / ${rightValue}`
445
+ : leftValue || rightValue || legacyValue;
446
+ const leftClassNote = annotationData.leftClassNote?.trim();
447
+ const rightClassNote = annotationData.rightClassNote?.trim();
448
+ const legacyClassNote = annotationData.classNote?.trim();
449
+ const displayClassNote =
450
+ leftClassNote && rightClassNote && leftClassNote !== rightClassNote
451
+ ? `${leftClassNote} / ${rightClassNote}`
452
+ : leftClassNote || rightClassNote || legacyClassNote;
453
+ return safeText(displayValue || '') + (displayClassNote ? ` (${safeText(displayClassNote)})` : '');
454
+ })()}
422
455
  </div>
423
456
  </div>
424
457
  ` : '<div class="class-annotation"></div>'}
425
458
 
426
- ${annotationData && annotationsSet?.has('class') && annotationData.hasSubclass ? `
459
+ ${annotationData && annotationsSet?.has('item') && (annotationData.leftHasSubclass || annotationData.rightHasSubclass || annotationData.hasSubclass) ? `
427
460
  <div class="subclass-annotation">
428
461
  <div class="subclass-text">
429
462
  POTENTIAL SUBCLASS
@@ -434,7 +467,7 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
434
467
  </div>
435
468
  ` : ''}
436
469
 
437
- ${annotationData && ((annotationData.includeConfirmation === true) || annotationData.additionalNotes) ? `
470
+ ${hasConfirmationOrNotes ? `
438
471
  <div class="confirmation-section">
439
472
  ${annotationData && (annotationData.includeConfirmation === true) ? `
440
473
  <div class="confirmation-summary">
@@ -465,10 +498,34 @@ export const renderReport: ReportRenderer = (data: PDFGenerationData): string =>
465
498
  </div>
466
499
  ` : ''}
467
500
 
468
- ${annotationData && annotationsSet?.has('notes') && annotationData.additionalNotes && annotationData.additionalNotes.trim() ? `
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>
501
+ ${annotationData && annotationsSet?.has('notes') && hasAdditionalNotes ? `
502
+ <section class="${notesShouldStartNewPage ? 'notes-page' : ''}">
503
+ ${hasSideAdditionalNotes ? `
504
+ ${leftAdditionalNotes && rightAdditionalNotes ? `
505
+ <div class="additional-notes-grid">
506
+ <div class="additional-notes-section additional-notes-section--half">
507
+ <h2 class="additional-notes-title">Additional Notes (L)</h2>
508
+ <p class="additional-notes-body">${escapeHtml(leftAdditionalNotes)}</p>
509
+ </div>
510
+ <div class="additional-notes-section additional-notes-section--half">
511
+ <h2 class="additional-notes-title">Additional Notes (R)</h2>
512
+ <p class="additional-notes-body">${escapeHtml(rightAdditionalNotes)}</p>
513
+ </div>
514
+ </div>
515
+ ` : `
516
+ <div class="additional-notes-section">
517
+ <h2 class="additional-notes-title">Additional Notes (${leftAdditionalNotes ? 'L' : 'R'})</h2>
518
+ <p class="additional-notes-body">${escapeHtml(leftAdditionalNotes || rightAdditionalNotes)}</p>
519
+ </div>
520
+ `}
521
+ ` : ''}
522
+
523
+ ${hasGeneralAdditionalNotes ? `
524
+ <div class="additional-notes-section" style="margin-top: ${hasSideAdditionalNotes ? '12px' : '0'};">
525
+ <h2 class="additional-notes-title">Additional Notes (General)</h2>
526
+ <p class="additional-notes-body">${escapeHtml(generalAdditionalNotes)}</p>
527
+ </div>
528
+ ` : ''}
472
529
  </section>
473
530
  ` : ''}
474
531
  </div>
@@ -33,7 +33,17 @@ export interface AnnotationData {
33
33
  // ID/Support level annotations
34
34
  supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
35
35
 
36
- // Class annotations
36
+ // Class annotations (left/right per-item)
37
+ leftItemType?: string;
38
+ leftCustomClass?: string;
39
+ leftClassNote?: string;
40
+ leftHasSubclass?: boolean;
41
+ rightItemType?: string;
42
+ rightCustomClass?: string;
43
+ rightClassNote?: string;
44
+ rightHasSubclass?: boolean;
45
+ // Legacy (kept for backward compatibility)
46
+ itemType?: string;
37
47
  classType?: string;
38
48
  customClass?: string;
39
49
  classNote?: string;
@@ -44,6 +54,8 @@ export interface AnnotationData {
44
54
  confirmationData?: ConfirmationData;
45
55
 
46
56
  // Notes
57
+ leftAdditionalNotes?: string;
58
+ rightAdditionalNotes?: string;
47
59
  additionalNotes?: string;
48
60
  }
49
61