@striae-org/striae 6.1.0 → 6.1.2

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.
@@ -26,6 +26,11 @@
26
26
  border-color: #adb5bd;
27
27
  }
28
28
 
29
+ .toggleButton:disabled {
30
+ cursor: not-allowed;
31
+ opacity: 0.6;
32
+ }
33
+
29
34
  .colorWheel {
30
35
  width: 180px;
31
36
  height: 40px;
@@ -35,6 +40,11 @@
35
40
  cursor: pointer;
36
41
  }
37
42
 
43
+ .colorWheel:disabled {
44
+ cursor: not-allowed;
45
+ opacity: 0.6;
46
+ }
47
+
38
48
  .colorGrid {
39
49
  display: grid;
40
50
  grid-template-columns: repeat(5, 1fr);
@@ -54,6 +64,15 @@
54
64
  transform: scale(1.1);
55
65
  }
56
66
 
67
+ .colorSwatch:disabled {
68
+ cursor: not-allowed;
69
+ opacity: 0.6;
70
+ }
71
+
72
+ .colorSwatch:disabled:hover {
73
+ transform: none;
74
+ }
75
+
57
76
  .colorSwatch.selected {
58
77
  border-color: #0d6efd;
59
78
  box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
@@ -4,6 +4,7 @@ import styles from './colors.module.css';
4
4
  interface ColorSelectorProps {
5
5
  selectedColor: string;
6
6
  onColorSelect: (color: string) => void;
7
+ disabled?: boolean;
7
8
  }
8
9
 
9
10
  interface ColorOption {
@@ -24,7 +25,7 @@ const commonColors: ColorOption[] = [
24
25
  { value: '#ffffff', label: 'White' }
25
26
  ];
26
27
 
27
- export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorProps) => {
28
+ export const ColorSelector = ({ selectedColor, onColorSelect, disabled = false }: ColorSelectorProps) => {
28
29
  const [showColorWheel, setShowColorWheel] = useState(false);
29
30
 
30
31
  return (
@@ -34,6 +35,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
34
35
  <button
35
36
  onClick={() => setShowColorWheel(!showColorWheel)}
36
37
  className={styles.toggleButton}
38
+ disabled={disabled}
37
39
  >
38
40
  {showColorWheel ? 'Presets' : 'Color Wheel'}
39
41
  </button>
@@ -47,6 +49,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
47
49
  onChange={(e) => onColorSelect(e.target.value)}
48
50
  className={styles.colorWheel}
49
51
  title="Choose a color"
52
+ disabled={disabled}
50
53
  />
51
54
  </>
52
55
  ) : (
@@ -59,6 +62,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
59
62
  onClick={() => onColorSelect(color.value)}
60
63
  aria-label={`Select ${color.label}`}
61
64
  title={color.label}
65
+ disabled={disabled}
62
66
  />
63
67
  ))}
64
68
  </div>
@@ -4,7 +4,7 @@ import { SignOut } from '../actions/signout';
4
4
  import { ManageProfile } from '../user/manage-profile';
5
5
  import { CaseImport } from './case-import/case-import';
6
6
  import { AuthContext } from '~/contexts/auth.context';
7
- import { getUserData } from '~/utils/data';
7
+ import { getUserData, getNotesViewPermission, getNotesButtonTooltip } from '~/utils/data';
8
8
  import { type ImportResult, type ConfirmationImportResult } from '~/types';
9
9
 
10
10
  interface NavbarProps {
@@ -117,11 +117,28 @@ export const Navbar = ({
117
117
  const disableLongRunningCaseActions = isUploading;
118
118
  const isCaseManagementActive = true;
119
119
  const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
120
- const canOpenImageNotes = hasLoadedImage;
121
- const isImageNotesReadOnly = isReadOnly || isCurrentImageConfirmed || isUploading;
122
- const isImageNotesActive = canOpenImageNotes;
123
120
  const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
124
121
  const isArchivedCase = Boolean(isReadOnly && archiveDetails?.archived);
122
+
123
+ // Use centralized permission helper for notes
124
+ const notesPermission = getNotesViewPermission({
125
+ imageLoaded: hasLoadedImage,
126
+ isUploading: isUploading || false,
127
+ isCheckingConfirmation: false, // Navbar doesn't track this granularly
128
+ isReadOnlyCase: isReadOnly || false,
129
+ isArchivedCase: isArchivedCase,
130
+ isConfirmedImage: isCurrentImageConfirmed || false
131
+ });
132
+
133
+ const imageNotesTitle = getNotesButtonTooltip(notesPermission, {
134
+ isReadOnlyCase: isReadOnly,
135
+ isArchivedCase: isArchivedCase,
136
+ isConfirmedImage: isCurrentImageConfirmed
137
+ });
138
+
139
+ const canOpenImageNotes = notesPermission.canOpen;
140
+ const isImageNotesActive = canOpenImageNotes;
141
+
125
142
  const caseExportLabel = isArchivedCase
126
143
  ? 'Export Archive'
127
144
  : isReadOnly
@@ -365,14 +382,12 @@ export const Navbar = ({
365
382
  className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
366
383
  disabled={!canOpenImageNotes}
367
384
  aria-pressed={isImageNotesActive}
368
- title={
369
- !hasLoadedImage
370
- ? 'Load an image to enable image notes'
371
- : isImageNotesReadOnly
372
- ? 'Image notes are view-only in this state'
373
- : undefined
374
- }
385
+ title={imageNotesTitle}
375
386
  onClick={() => {
387
+ if (!notesPermission.canOpen) {
388
+ return;
389
+ }
390
+
376
391
  onOpenImageNotes?.();
377
392
  }}
378
393
  >
@@ -11,7 +11,9 @@ import {
11
11
  import {
12
12
  canUploadFile,
13
13
  ensureCaseConfirmationSummary,
14
- getCaseConfirmationSummary
14
+ getCaseConfirmationSummary,
15
+ getNotesViewPermission,
16
+ getNotesButtonTooltip
15
17
  } from '~/utils/data';
16
18
  import { type FileData } from '~/types';
17
19
 
@@ -28,7 +30,6 @@ interface CaseSidebarProps {
28
30
  isReadOnly?: boolean;
29
31
  isReviewOnlyCase?: boolean;
30
32
  isArchivedCase?: boolean;
31
- isConfirmed?: boolean;
32
33
  confirmationSaveVersion?: number;
33
34
  selectedFileId?: string;
34
35
  isUploading?: boolean;
@@ -50,7 +51,6 @@ export const CaseSidebar = ({
50
51
  isReadOnly = false,
51
52
  isReviewOnlyCase = false,
52
53
  isArchivedCase = false,
53
- isConfirmed = false,
54
54
  confirmationSaveVersion = 0,
55
55
  selectedFileId,
56
56
  isUploading = false,
@@ -238,27 +238,24 @@ const handleImageSelect = (file: FileData) => {
238
238
  selectedFileId && !selectedFileConfirmationState
239
239
  );
240
240
 
241
- const isSelectedFileConfirmed =
242
- isConfirmed || !!selectedFileConfirmationState?.isConfirmed;
243
-
244
- const isImageNotesDisabled =
245
- !imageLoaded ||
246
- isReadOnly ||
247
- isSelectedFileConfirmed ||
248
- isUploading ||
249
- isCheckingSelectedFileConfirmation;
250
-
251
- const imageNotesTitle = isUploading
252
- ? 'Cannot edit notes while uploading'
253
- : isCheckingSelectedFileConfirmation
254
- ? 'Checking confirmation status...'
255
- : isSelectedFileConfirmed
256
- ? 'Cannot edit notes for confirmed images'
257
- : isReadOnly
258
- ? 'Cannot edit notes for read-only cases'
259
- : !imageLoaded
260
- ? 'Select an image first'
261
- : undefined;
241
+ const isSelectedFileConfirmed = !!selectedFileConfirmationState?.isConfirmed;
242
+
243
+ // Use centralized permission helper
244
+ const notesPermission = getNotesViewPermission({
245
+ imageLoaded,
246
+ isUploading,
247
+ isCheckingConfirmation: isCheckingSelectedFileConfirmation,
248
+ isReadOnlyCase: isReadOnly,
249
+ isArchivedCase: isArchivedCase,
250
+ isConfirmedImage: isSelectedFileConfirmed
251
+ });
252
+
253
+ const isImageNotesDisabled = !notesPermission.canOpen;
254
+ const imageNotesTitle = getNotesButtonTooltip(notesPermission, {
255
+ isReadOnlyCase: isReadOnly,
256
+ isArchivedCase: isArchivedCase,
257
+ isConfirmedImage: isSelectedFileConfirmed
258
+ });
262
259
 
263
260
  const showCaseExportButton = Boolean(currentCase && isReadOnly);
264
261
  const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
@@ -5,7 +5,7 @@ import { AddlNotesModal } from './addl-notes-modal';
5
5
  import { ItemDetailsModal } from './item-details/item-details-modal';
6
6
  import { buildItemDetailsSummary } from './item-details/item-details-shared';
7
7
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
8
- import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData, type ItemType } from '~/types/annotations';
8
+ import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData, type ItemType, type SupportLevel, type IndexType } from '~/types/annotations';
9
9
  import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
10
10
  import { auditService } from '~/services/audit';
11
11
  import styles from './notes.module.css';
@@ -23,9 +23,6 @@ interface NotesEditorFormProps {
23
23
  onRegisterSaveHandler?: (saveHandler: (() => Promise<boolean>) | null) => void;
24
24
  }
25
25
 
26
- type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
27
- type IndexType = 'number' | 'color';
28
-
29
26
  interface NotesFormSnapshot {
30
27
  leftCase: string;
31
28
  rightCase: string;
@@ -154,7 +151,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
154
151
  const [savedSnapshot, setSavedSnapshot] = useState<string>('');
155
152
  const [hasLoadedSnapshot, setHasLoadedSnapshot] = useState(false);
156
153
  const [isDirty, setIsDirty] = useState(false);
157
- const areInputsDisabled = isUploading || isConfirmedImage || isReadOnly;
154
+ const areEditsDisabled = isUploading || isReadOnly || isConfirmedImage;
155
+ const isReadOnlyMode = isConfirmedImage || isReadOnly;
156
+ const canOpenModals = !isUploading;
158
157
 
159
158
  const notificationHandler = useCallback((message: string, type: 'success' | 'error' | 'warning' = 'success') => {
160
159
  if (externalShowNotification) {
@@ -687,7 +686,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
687
686
  type="text"
688
687
  value={leftCase}
689
688
  onChange={(e) => setLeftCase(e.target.value)}
690
- disabled={useCurrentCaseLeft || areInputsDisabled}
689
+ disabled={useCurrentCaseLeft || areEditsDisabled}
691
690
  />
692
691
  </div>
693
692
  <label className={`${styles.checkboxLabel} mb-4`}>
@@ -696,7 +695,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
696
695
  checked={useCurrentCaseLeft}
697
696
  onChange={(e) => setUseCurrentCaseLeft(e.target.checked)}
698
697
  className={styles.checkbox}
699
- disabled={areInputsDisabled}
698
+ disabled={areEditsDisabled}
700
699
  />
701
700
  <span>Use current case number</span>
702
701
  </label>
@@ -707,7 +706,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
707
706
  type="text"
708
707
  value={leftItem}
709
708
  onChange={(e) => setLeftItem(e.target.value)}
710
- disabled={areInputsDisabled}
709
+ disabled={areEditsDisabled}
711
710
  />
712
711
  </div>
713
712
  </div>
@@ -720,7 +719,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
720
719
  type="text"
721
720
  value={rightCase}
722
721
  onChange={(e) => setRightCase(e.target.value)}
723
- disabled={useCurrentCaseRight || areInputsDisabled}
722
+ disabled={useCurrentCaseRight || areEditsDisabled}
724
723
  />
725
724
  </div>
726
725
  <label className={`${styles.checkboxLabel} mb-4`}>
@@ -729,7 +728,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
729
728
  checked={useCurrentCaseRight}
730
729
  onChange={(e) => setUseCurrentCaseRight(e.target.checked)}
731
730
  className={styles.checkbox}
732
- disabled={areInputsDisabled}
731
+ disabled={areEditsDisabled}
733
732
  />
734
733
  <span>Use current case number</span>
735
734
  </label>
@@ -740,7 +739,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
740
739
  type="text"
741
740
  value={rightItem}
742
741
  onChange={(e) => setRightItem(e.target.value)}
743
- disabled={areInputsDisabled}
742
+ disabled={areEditsDisabled}
744
743
  />
745
744
  </div>
746
745
  </div>
@@ -751,6 +750,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
751
750
  <ColorSelector
752
751
  selectedColor={caseFontColor}
753
752
  onColorSelect={setCaseFontColor}
753
+ disabled={areEditsDisabled}
754
754
  />
755
755
  </div>
756
756
  </>
@@ -795,7 +795,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
795
795
  value={getSelectedItemData().itemType}
796
796
  onChange={(e) => setSelectedItemData({ itemType: e.target.value as ItemType })}
797
797
  className={styles.select}
798
- disabled={areInputsDisabled}
798
+ disabled={areEditsDisabled}
799
799
  >
800
800
  <option value="">Select item type...</option>
801
801
  <option value="Bullet">Bullet</option>
@@ -810,7 +810,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
810
810
  value={getSelectedItemData().customClass}
811
811
  onChange={(e) => setSelectedItemData({ customClass: e.target.value })}
812
812
  placeholder="Specify object type"
813
- disabled={areInputsDisabled}
813
+ disabled={areEditsDisabled}
814
814
  />
815
815
  )}
816
816
 
@@ -819,7 +819,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
819
819
  onChange={(e) => setSelectedItemData({ classNote: e.target.value })}
820
820
  placeholder="Enter item details..."
821
821
  className={styles.textarea}
822
- disabled={areInputsDisabled}
822
+ disabled={areEditsDisabled}
823
823
  />
824
824
  </div>
825
825
  </div>
@@ -838,7 +838,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
838
838
  checked={getSelectedItemData().hasSubclass}
839
839
  onChange={(e) => setSelectedItemData({ hasSubclass: e.target.checked })}
840
840
  className={styles.checkbox}
841
- disabled={areInputsDisabled}
841
+ disabled={areEditsDisabled}
842
842
  />
843
843
  <span>Potential subclass?</span>
844
844
  </label>
@@ -866,7 +866,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
866
866
  type="radio"
867
867
  checked={indexType === 'color'}
868
868
  onChange={() => setIndexType('color')}
869
- disabled={areInputsDisabled}
869
+ disabled={areEditsDisabled}
870
870
  />
871
871
  <span>Color</span>
872
872
  </label>
@@ -875,7 +875,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
875
875
  type="radio"
876
876
  checked={indexType === 'number'}
877
877
  onChange={() => setIndexType('number')}
878
- disabled={areInputsDisabled}
878
+ disabled={areEditsDisabled}
879
879
  />
880
880
  <span>Number/Letter</span>
881
881
  </label>
@@ -887,12 +887,13 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
887
887
  value={indexNumber}
888
888
  onChange={(e) => setIndexNumber(e.target.value)}
889
889
  placeholder="Enter index number"
890
- disabled={areInputsDisabled}
890
+ disabled={areEditsDisabled}
891
891
  />
892
892
  ) : indexType === 'color' ? (
893
893
  <ColorSelector
894
894
  selectedColor={indexColor}
895
895
  onColorSelect={setIndexColor}
896
+ disabled={areEditsDisabled}
896
897
  />
897
898
  ) : null}
898
899
  </div>
@@ -926,7 +927,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
926
927
  }
927
928
  }}
928
929
  className={styles.select}
929
- disabled={areInputsDisabled}
930
+ disabled={areEditsDisabled}
930
931
  >
931
932
  <option value="">Select support level...</option>
932
933
  <option value="ID">Identification</option>
@@ -939,7 +940,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
939
940
  checked={includeConfirmation}
940
941
  onChange={(e) => setIncludeConfirmation(e.target.checked)}
941
942
  className={styles.checkbox}
942
- disabled={areInputsDisabled}
943
+ disabled={areEditsDisabled}
943
944
  />
944
945
  <span>Include confirmation field</span>
945
946
  </label>
@@ -953,7 +954,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
953
954
  <button
954
955
  onClick={() => setIsModalOpen(true)}
955
956
  className={styles.notesButton}
956
- title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
957
+ disabled={!canOpenModals}
958
+ title={isUploading ? "Cannot open notes while uploading" : undefined}
957
959
  >
958
960
  Additional Notes
959
961
  </button>
@@ -963,8 +965,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
963
965
  <button
964
966
  onClick={handleSave}
965
967
  className={styles.saveButton}
966
- disabled={areInputsDisabled}
967
- title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
968
+ disabled={areEditsDisabled}
969
+ title={isConfirmedImage ? "Cannot save notes - image is confirmed" : isUploading ? "Cannot save notes while uploading" : isReadOnly ? "Cannot save notes - case is read-only" : undefined}
968
970
  >
969
971
  Save Notes
970
972
  </button>
@@ -974,13 +976,13 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
974
976
  onClose={() => setIsModalOpen(false)}
975
977
  notes={additionalNotes}
976
978
  onSave={(notes) => {
977
- if (areInputsDisabled) {
979
+ if (areEditsDisabled) {
978
980
  return;
979
981
  }
980
982
 
981
983
  setAdditionalNotes(notes);
982
984
  }}
983
- isReadOnly={areInputsDisabled}
985
+ isReadOnly={isReadOnlyMode}
984
986
  showNotification={notificationHandler}
985
987
  />
986
988
  <ItemDetailsModal
@@ -991,7 +993,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
991
993
  cartridgeCaseData={getSelectedItemData().cartridgeCaseData}
992
994
  shotshellData={getSelectedItemData().shotshellData}
993
995
  onSave={(b, c, s) => {
994
- if (areInputsDisabled) {
996
+ if (areEditsDisabled) {
995
997
  return;
996
998
  }
997
999
 
@@ -1010,7 +1012,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
1010
1012
  }
1011
1013
  }}
1012
1014
  showNotification={notificationHandler}
1013
- isReadOnly={areInputsDisabled}
1015
+ isReadOnly={isReadOnlyMode}
1014
1016
  />
1015
1017
  </>
1016
1018
  )}
@@ -26,7 +26,6 @@ interface SidebarContainerProps {
26
26
  isReadOnly?: boolean;
27
27
  isReviewOnlyCase?: boolean;
28
28
  isArchivedCase?: boolean;
29
- isConfirmed?: boolean;
30
29
  confirmationSaveVersion?: number;
31
30
  isUploading?: boolean;
32
31
  onUploadStatusChange?: (isUploading: boolean) => void;
@@ -22,7 +22,6 @@ interface SidebarProps {
22
22
  isReadOnly?: boolean;
23
23
  isReviewOnlyCase?: boolean;
24
24
  isArchivedCase?: boolean;
25
- isConfirmed?: boolean;
26
25
  confirmationSaveVersion?: number;
27
26
  isUploading?: boolean;
28
27
  onUploadStatusChange?: (isUploading: boolean) => void;
@@ -43,7 +42,6 @@ export const Sidebar = ({
43
42
  isReadOnly = false,
44
43
  isReviewOnlyCase = false,
45
44
  isArchivedCase = false,
46
- isConfirmed = false,
47
45
  confirmationSaveVersion = 0,
48
46
  isUploading: initialIsUploading = false,
49
47
  onUploadStatusChange,
@@ -93,7 +91,6 @@ export const Sidebar = ({
93
91
  isReadOnly={isReadOnly}
94
92
  isReviewOnlyCase={isReviewOnlyCase}
95
93
  isArchivedCase={isArchivedCase}
96
- isConfirmed={isConfirmed}
97
94
  confirmationSaveVersion={confirmationSaveVersion}
98
95
  selectedFileId={imageId}
99
96
  isUploading={isUploading}
@@ -746,10 +746,10 @@ export const Striae = ({ user }: StriaePage) => {
746
746
  const isCurrentImageConfirmed = hasLoadedImage && !!annotationData?.confirmationData;
747
747
 
748
748
  useEffect(() => {
749
- if (showNotes && (!hasLoadedImage || isCurrentImageConfirmed)) {
749
+ if (showNotes && !hasLoadedImage) {
750
750
  setShowNotes(false);
751
751
  }
752
- }, [showNotes, hasLoadedImage, isCurrentImageConfirmed]);
752
+ }, [showNotes, hasLoadedImage]);
753
753
 
754
754
  // Automatic save handler for annotation updates
755
755
  const handleAnnotationUpdate = async (data: AnnotationData) => {
@@ -868,7 +868,6 @@ export const Striae = ({ user }: StriaePage) => {
868
868
  isReadOnly={isReadOnlyCase}
869
869
  isReviewOnlyCase={isReviewOnlyCase}
870
870
  isArchivedCase={archiveDetails.archived}
871
- isConfirmed={!!annotationData?.confirmationData}
872
871
  confirmationSaveVersion={confirmationSaveVersion}
873
872
  isUploading={isUploading}
874
873
  onUploadStatusChange={setIsUploading}
@@ -1,6 +1,7 @@
1
1
  // Annotation-related types and interfaces
2
2
 
3
- export type ItemType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
3
+ import type { ItemType, SupportLevel, IndexType } from '../../shared/types/annotation-literals';
4
+ export type { ItemType, SupportLevel, IndexType };
4
5
 
5
6
  export interface BoxAnnotation {
6
7
  id: string;
@@ -100,10 +101,10 @@ export interface AnnotationData {
100
101
  cartridgeCaseData?: CartridgeCaseAnnotationData;
101
102
  shotshellData?: ShotshellAnnotationData;
102
103
  hasSubclass?: boolean;
103
- indexType?: 'number' | 'color';
104
+ indexType?: IndexType;
104
105
  indexNumber?: string;
105
106
  indexColor?: string;
106
- supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
107
+ supportLevel?: SupportLevel;
107
108
  includeConfirmation: boolean;
108
109
  confirmationData?: ConfirmationData;
109
110
  leftAdditionalNotes?: string;
@@ -26,6 +26,16 @@ export interface CaseMetadata {
26
26
  createdAt: string;
27
27
  }
28
28
 
29
+ /**
30
+ * Result for notes viewing permissions
31
+ * Determines if notes can be viewed and in what mode (edit or view-only)
32
+ */
33
+ export interface NotesViewPermission {
34
+ canOpen: boolean; // Can the notes panel be opened
35
+ isReadOnly: boolean; // Are notes in read-only mode (can view but not edit)
36
+ reason?: string; // Reason if notes cannot be opened
37
+ }
38
+
29
39
  /**
30
40
  * Get user data from KV store
31
41
  */
@@ -565,4 +575,117 @@ export const removeUserCase = async (user: User, caseNumber: string): Promise<vo
565
575
  console.error('Error removing case from user:', error);
566
576
  throw error;
567
577
  }
578
+ };
579
+
580
+ // ============================================================================
581
+ // NOTES VIEW PERMISSIONS
582
+ // ============================================================================
583
+
584
+ /**
585
+ * Determine if notes can be opened and viewed, and whether they should be in read-only mode
586
+ *
587
+ * Notes can be viewed in the following scenarios:
588
+ * - Normal active case with unconfirmed image: Can edit and save
589
+ * - Active case with confirmed image: Can view only (read-only)
590
+ * - Read-only case (review/confirmation case): Can view only (read-only)
591
+ * - Archived case: Can view only (read-only)
592
+ *
593
+ * Notes cannot be opened when:
594
+ * - Files are uploading
595
+ * - Confirmation status is still being checked
596
+ * - No image is loaded
597
+ *
598
+ * @param config Configuration object with state flags
599
+ * @returns NotesViewPermission object indicating if notes can be opened and if they're read-only
600
+ */
601
+ export const getNotesViewPermission = (config: {
602
+ imageLoaded: boolean;
603
+ isUploading: boolean;
604
+ isCheckingConfirmation: boolean;
605
+ isReadOnlyCase?: boolean;
606
+ isArchivedCase?: boolean;
607
+ isConfirmedImage?: boolean;
608
+ }): NotesViewPermission => {
609
+ const {
610
+ imageLoaded,
611
+ isUploading,
612
+ isCheckingConfirmation,
613
+ isReadOnlyCase = false,
614
+ isArchivedCase = false,
615
+ isConfirmedImage = false
616
+ } = config;
617
+
618
+ // Cannot open if uploading files
619
+ if (isUploading) {
620
+ return {
621
+ canOpen: false,
622
+ isReadOnly: false,
623
+ reason: 'Cannot open notes while uploading'
624
+ };
625
+ }
626
+
627
+ // Cannot open if checking confirmation status
628
+ if (isCheckingConfirmation) {
629
+ return {
630
+ canOpen: false,
631
+ isReadOnly: false,
632
+ reason: 'Checking confirmation status...'
633
+ };
634
+ }
635
+
636
+ // Cannot open if no image is loaded
637
+ if (!imageLoaded) {
638
+ return {
639
+ canOpen: false,
640
+ isReadOnly: false,
641
+ reason: 'Select an image first'
642
+ };
643
+ }
644
+
645
+ // Can open, determine if read-only
646
+ const isReadOnly = isConfirmedImage || isReadOnlyCase || isArchivedCase;
647
+
648
+ return {
649
+ canOpen: true,
650
+ isReadOnly
651
+ };
652
+ };
653
+
654
+ /**
655
+ * Get a user-friendly tooltip message for the Image Notes button
656
+ *
657
+ * This centralizes all the tooltip logic that appears in the navbar and sidebar
658
+ *
659
+ * @param permission NotesViewPermission object from getNotesViewPermission
660
+ * @param additionalContext Optional context for more specific messages
661
+ * @returns Tooltip string, or undefined if button has no specific tooltip
662
+ */
663
+ export const getNotesButtonTooltip = (
664
+ permission: NotesViewPermission,
665
+ additionalContext?: {
666
+ isReadOnlyCase?: boolean;
667
+ isArchivedCase?: boolean;
668
+ isConfirmedImage?: boolean;
669
+ }
670
+ ): string | undefined => {
671
+ // If cannot open, return the reason
672
+ if (!permission.canOpen) {
673
+ return permission.reason;
674
+ }
675
+
676
+ // Can open - provide context-specific read-only messages
677
+ if (permission.isReadOnly && additionalContext) {
678
+ if (additionalContext.isConfirmedImage) {
679
+ return 'Image notes: viewing only (image is confirmed)';
680
+ }
681
+ if (additionalContext.isReadOnlyCase) {
682
+ return 'Image notes: viewing only (case is read-only)';
683
+ }
684
+ if (additionalContext.isArchivedCase) {
685
+ return 'Image notes: viewing only (case is archived)';
686
+ }
687
+ }
688
+
689
+ // No tooltip needed for normal edit mode
690
+ return undefined;
568
691
  };
package/package.json CHANGED
@@ -1,140 +1,141 @@
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"
1
+ {
2
+ "name": "@striae-org/striae",
3
+ "version": "6.1.2",
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
+ "shared/",
45
+ "!workers/*/.wrangler",
46
+ "!workers/*/package-lock.json",
47
+ "!workers/*/worker-configuration.d.ts",
48
+ "!workers/*/wrangler.jsonc",
49
+ "!workers/*/src/**/*worker.ts",
50
+ "!workers/pdf-worker/src/assets/**/*",
51
+ "workers/pdf-worker/src/assets/generated-assets.example.ts",
52
+ "!workers/pdf-worker/src/formats/**/*",
53
+ "workers/pdf-worker/src/formats/format-striae.ts",
54
+ ".env.example",
55
+ "firebase.json",
56
+ "tsconfig.json",
57
+ "vite.config.ts",
58
+ "/worker-configuration.d.ts",
59
+ "wrangler.toml.example",
60
+ "LICENSE"
61
+ ],
62
+ "sideEffects": false,
63
+ "type": "module",
64
+ "scripts": {
65
+ "deploy:all": "bash ./scripts/deploy-all.sh",
66
+ "emulators": "firebase emulators:start --only auth",
67
+ "dev": "node ./scripts/dev.cjs && react-router dev",
68
+ "build": "node ./scripts/dev.cjs && react-router build",
69
+ "clean": "rm -rf build node_modules/.cache .cache",
70
+ "clean:build": "npm run clean && npm run build",
71
+ "deploy": "npm run build && wrangler pages deploy",
72
+ "publish:npm": "npm publish --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
73
+ "publish:npm:dry-run": "npm publish --dry-run --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
74
+ "publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
75
+ "publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
76
+ "publish:all": "npm run publish:npm && npm run publish:github",
77
+ "publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
78
+ "lint": "node ./scripts/run-eslint.cjs",
79
+ "start": "node ./scripts/dev.cjs && wrangler pages dev",
80
+ "typecheck": "react-router typegen && tsc",
81
+ "typegen": "wrangler types",
82
+ "preview": "npm run build && wrangler pages dev",
83
+ "cf-typegen": "wrangler types",
84
+ "enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
85
+ "unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
86
+ "update-versions": "node ./scripts/update-markdown-versions.cjs",
87
+ "update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
88
+ "deploy-config": "bash ./scripts/deploy-config.sh",
89
+ "update-env": "bash ./scripts/deploy-config.sh --update-env",
90
+ "install-workers": "bash ./scripts/install-workers.sh",
91
+ "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",
92
+ "deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
93
+ "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
94
+ "deploy-pages": "bash ./scripts/deploy-pages.sh",
95
+ "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
96
+ "deploy-members": "bash ./scripts/deploy-members-emails.sh",
97
+ "deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
98
+ "deploy-workers:data": "cd workers/data-worker && npm run deploy",
99
+ "deploy-workers:image": "cd workers/image-worker && npm run deploy",
100
+ "deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
101
+ "deploy-workers:user": "cd workers/user-worker && npm run deploy"
102
+ },
103
+ "dependencies": {
104
+ "@react-router/cloudflare": "^7.14.1",
105
+ "firebase": "^12.12.0",
106
+ "isbot": "^5.1.38",
107
+ "jszip": "^3.10.1",
108
+ "qrcode": "^1.5.4",
109
+ "react": "^19.2.5",
110
+ "react-dom": "^19.2.5",
111
+ "react-router": "^7.14.1"
112
+ },
113
+ "devDependencies": {
114
+ "@react-router/dev": "^7.14.1",
115
+ "@react-router/fs-routes": "^7.14.1",
116
+ "@types/qrcode": "^1.5.6",
117
+ "@types/react": "^19.2.14",
118
+ "@types/react-dom": "^19.2.3",
119
+ "@typescript-eslint/eslint-plugin": "^8.58.2",
120
+ "@typescript-eslint/parser": "^8.58.2",
121
+ "eslint": "^9.39.4",
122
+ "eslint-import-resolver-typescript": "^4.4.4",
123
+ "eslint-plugin-import": "^2.32.0",
124
+ "eslint-plugin-jsx-a11y": "^6.10.2",
125
+ "eslint-plugin-react": "^7.37.5",
126
+ "eslint-plugin-react-hooks": "^7.0.1",
127
+ "firebase-admin": "^13.8.0",
128
+ "modern-normalize": "^3.0.1",
129
+ "typescript": "^5.9.3",
130
+ "vite": "^7.3.2",
131
+ "vite-tsconfig-paths": "^6.1.1",
132
+ "wrangler": "^4.83.0"
133
+ },
134
+ "overrides": {
135
+ "@tootallnate/once": "3.0.1"
136
+ },
137
+ "engines": {
138
+ "node": ">=20.19.0"
139
+ },
140
+ "packageManager": "npm@11.12.0"
140
141
  }
@@ -0,0 +1,5 @@
1
+ // Shared annotation literal unions used by app and workers.
2
+
3
+ export type ItemType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
4
+ export type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
5
+ export type IndexType = 'number' | 'color';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-worker",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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.82.2"
11
+ "wrangler": "^4.83.0"
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-14",
10
+ "compatibility_date": "2026-04-15",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-worker",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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.82.2"
11
+ "wrangler": "^4.83.0"
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-14",
8
+ "compatibility_date": "2026-04-15",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-worker",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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.82.2"
11
+ "wrangler": "^4.83.0"
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-14",
5
+ "compatibility_date": "2026-04-15",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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.82.2"
12
+ "wrangler": "^4.83.0"
13
13
  }
14
14
  }
@@ -1,3 +1,5 @@
1
+ import type { IndexType, SupportLevel } from '../../../shared/types/annotation-literals';
2
+
1
3
  export interface BoxAnnotation {
2
4
  x: number;
3
5
  y: number;
@@ -16,7 +18,7 @@ export interface ConfirmationData {
16
18
 
17
19
  export interface AnnotationData {
18
20
  // Index annotations
19
- indexType?: 'number' | 'color';
21
+ indexType?: IndexType;
20
22
  indexNumber?: string;
21
23
  indexColor?: string;
22
24
 
@@ -31,7 +33,7 @@ export interface AnnotationData {
31
33
  boxAnnotations?: BoxAnnotation[];
32
34
 
33
35
  // ID/Support level annotations
34
- supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
36
+ supportLevel?: SupportLevel;
35
37
 
36
38
  // Class annotations (left/right per-item)
37
39
  leftItemType?: string;
@@ -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-04-14",
5
+ "compatibility_date": "2026-04-15",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-worker",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
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.82.2"
11
+ "wrangler": "^4.83.0"
12
12
  }
13
13
  }
@@ -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-04-14",
5
+ "compatibility_date": "2026-04-15",
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-04-14"
3
+ compatibility_date = "2026-04-15"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6