@striae-org/striae 3.0.4 → 3.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 (91) hide show
  1. package/NOTICE +0 -5
  2. package/app/components/actions/case-export/core-export.ts +1 -1
  3. package/app/components/actions/case-export/download-handlers.ts +10 -12
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/confirmation-import.ts +24 -9
  6. package/app/components/actions/case-import/orchestrator.ts +3 -4
  7. package/app/components/actions/case-import/validation.ts +3 -3
  8. package/app/components/actions/case-import/zip-processing.ts +12 -48
  9. package/app/components/actions/case-manage.ts +0 -1
  10. package/app/components/actions/confirm-export.ts +2 -2
  11. package/app/components/audit/user-audit-viewer.tsx +53 -15
  12. package/app/components/audit/user-audit.module.css +11 -4
  13. package/app/components/canvas/box-annotations/box-annotations.tsx +36 -7
  14. package/app/components/canvas/canvas.tsx +35 -24
  15. package/app/components/canvas/confirmation/confirmation.module.css +5 -2
  16. package/app/components/canvas/confirmation/confirmation.tsx +25 -8
  17. package/app/components/sidebar/case-export/case-export.module.css +194 -5
  18. package/app/components/sidebar/case-export/case-export.tsx +291 -11
  19. package/app/components/sidebar/case-import/case-import.module.css +9 -5
  20. package/app/components/sidebar/case-import/case-import.tsx +30 -7
  21. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +2 -2
  22. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  23. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +1 -1
  24. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +34 -9
  25. package/app/components/sidebar/cases/case-sidebar.tsx +13 -13
  26. package/app/components/sidebar/cases/cases-modal.tsx +12 -2
  27. package/app/components/sidebar/files/files-modal.tsx +28 -8
  28. package/app/components/sidebar/sidebar.module.css +2 -3
  29. package/app/components/sidebar/sidebar.tsx +1 -16
  30. package/app/components/sidebar/upload/image-upload-zone.tsx +4 -4
  31. package/app/components/toolbar/toolbar-color-selector.module.css +2 -2
  32. package/app/components/toolbar/toolbar-color-selector.tsx +3 -3
  33. package/app/components/toolbar/toolbar.tsx +19 -9
  34. package/app/components/user/delete-account.module.css +4 -1
  35. package/app/components/user/delete-account.tsx +22 -3
  36. package/app/components/user/manage-profile.tsx +0 -2
  37. package/app/entry.server.tsx +2 -3
  38. package/app/hooks/useInactivityTimeout.ts +5 -1
  39. package/app/root.tsx +0 -3
  40. package/app/routes/_index.tsx +1 -16
  41. package/app/routes/auth/emailVerification.tsx +1 -1
  42. package/app/routes/auth/login.tsx +7 -5
  43. package/app/routes/auth/route.ts +3 -12
  44. package/app/routes/striae/striae.tsx +1 -1
  45. package/app/services/audit.service.ts +29 -9
  46. package/app/tailwind.css +16 -1
  47. package/app/types/audit.ts +3 -3
  48. package/app/types/case.ts +1 -1
  49. package/app/types/import.ts +0 -2
  50. package/app/utils/SHA256.ts +3 -3
  51. package/app/utils/batch-operations.ts +6 -6
  52. package/app/utils/data-operations.ts +14 -7
  53. package/app/utils/permissions.ts +0 -2
  54. package/functions/[[path]].ts +0 -1
  55. package/package.json +1 -2
  56. package/public/_headers +0 -12
  57. package/public/assets/striae.jpg +0 -0
  58. package/scripts/deploy-config.sh +0 -7
  59. package/scripts/run-eslint.cjs +14 -6
  60. package/worker-configuration.d.ts +2 -2
  61. package/workers/audit-worker/src/audit-worker.example.ts +9 -7
  62. package/workers/audit-worker/worker-configuration.d.ts +2 -2
  63. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  64. package/workers/data-worker/src/data-worker.example.ts +1 -1
  65. package/workers/data-worker/worker-configuration.d.ts +2 -2
  66. package/workers/data-worker/wrangler.jsonc.example +1 -1
  67. package/workers/image-worker/worker-configuration.d.ts +2 -2
  68. package/workers/image-worker/wrangler.jsonc.example +1 -1
  69. package/workers/keys-worker/worker-configuration.d.ts +2 -2
  70. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  71. package/workers/pdf-worker/src/pdf-worker.example.ts +3 -3
  72. package/workers/pdf-worker/worker-configuration.d.ts +7448 -7448
  73. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  74. package/workers/user-worker/src/user-worker.example.ts +10 -10
  75. package/workers/user-worker/worker-configuration.d.ts +2 -2
  76. package/workers/user-worker/wrangler.jsonc.example +1 -1
  77. package/wrangler.toml.example +1 -1
  78. package/app/components/sidebar/hash/hash-utility.module.css +0 -366
  79. package/app/components/sidebar/hash/hash-utility.tsx +0 -982
  80. package/app/config-example/meta-config.json +0 -6
  81. package/app/routes/mobile-prevented/mobilePrevented.module.css +0 -47
  82. package/app/routes/mobile-prevented/mobilePrevented.tsx +0 -26
  83. package/app/routes/mobile-prevented/route.ts +0 -14
  84. package/app/utils/device-detection.ts +0 -5
  85. package/app/utils/html-sanitizer.ts +0 -80
  86. package/app/utils/meta.ts +0 -48
  87. package/public/icon-256.png +0 -0
  88. package/public/icon-512.png +0 -0
  89. package/public/manifest.json +0 -25
  90. package/public/shortcut.png +0 -0
  91. package/public/social-image.png +0 -0
@@ -23,6 +23,11 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
23
23
  }>({});
24
24
  const CASES_PER_PAGE = 10;
25
25
 
26
+ const startLoading = () => {
27
+ setIsLoading(true);
28
+ setError('');
29
+ };
30
+
26
31
  useEffect(() => {
27
32
  const handleEscape = (e: KeyboardEvent) => {
28
33
  if (e.key === 'Escape') {
@@ -38,8 +43,9 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
38
43
 
39
44
  useEffect(() => {
40
45
  if (isOpen) {
41
- setIsLoading(true);
42
- setError('');
46
+ const loadingTimer = window.setTimeout(() => {
47
+ startLoading();
48
+ }, 0);
43
49
 
44
50
  listCases(user)
45
51
  .then(fetchedCases => {
@@ -52,6 +58,10 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
52
58
  .finally(() => {
53
59
  setIsLoading(false);
54
60
  });
61
+
62
+ return () => {
63
+ window.clearTimeout(loadingTimer);
64
+ };
55
65
  }
56
66
  }, [isOpen, user]);
57
67
 
@@ -41,7 +41,10 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
41
41
  // Fetch confirmation status only for currently visible paginated files
42
42
  useEffect(() => {
43
43
  const fetchConfirmationStatuses = async () => {
44
- const visibleFiles = currentFiles;
44
+ const visibleFiles = files.slice(
45
+ currentPage * FILES_PER_PAGE,
46
+ currentPage * FILES_PER_PAGE + FILES_PER_PAGE
47
+ );
45
48
 
46
49
  if (!isOpen || !currentCase || !user || visibleFiles.length === 0) {
47
50
  return;
@@ -84,6 +87,21 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
84
87
  fetchConfirmationStatuses();
85
88
  }, [isOpen, currentCase, currentPage, files, user]);
86
89
 
90
+ useEffect(() => {
91
+ const handleEscape = (event: KeyboardEvent) => {
92
+ if (event.key === 'Escape' && isOpen) {
93
+ onClose();
94
+ }
95
+ };
96
+
97
+ if (isOpen) {
98
+ document.addEventListener('keydown', handleEscape);
99
+ return () => {
100
+ document.removeEventListener('keydown', handleEscape);
101
+ };
102
+ }
103
+ }, [isOpen, onClose]);
104
+
87
105
  const handleFileSelect = (file: FileData) => {
88
106
  onFileSelect?.(file);
89
107
  onClose();
@@ -123,12 +141,6 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
123
141
  }
124
142
  };
125
143
 
126
- const handleKeyDown = (event: React.KeyboardEvent) => {
127
- if (event.key === 'Escape') {
128
- onClose();
129
- }
130
- };
131
-
132
144
  const formatDate = (dateString: string) => {
133
145
  try {
134
146
  return new Date(dateString).toLocaleDateString();
@@ -153,7 +165,7 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
153
165
  if (!isOpen) return null;
154
166
 
155
167
  return (
156
- <div className={styles.modalOverlay} onKeyDown={handleKeyDown} tabIndex={-1}>
168
+ <div className={styles.modalOverlay}>
157
169
  <div className={styles.modal}>
158
170
  <div className={styles.modalHeader}>
159
171
  <h2>Files in Case {currentCase}</h2>
@@ -188,6 +200,14 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
188
200
  key={file.id}
189
201
  className={`${styles.fileItem} ${selectedFileId === file.id ? styles.active : ''} ${confirmationClass}`}
190
202
  onClick={() => handleFileSelect(file)}
203
+ onKeyDown={(event) => {
204
+ if (event.key === 'Enter' || event.key === ' ') {
205
+ event.preventDefault();
206
+ handleFileSelect(file);
207
+ }
208
+ }}
209
+ role="button"
210
+ tabIndex={0}
191
211
  >
192
212
  <div className={styles.fileInfo}>
193
213
  <div className={styles.fileName} title={file.originalFilename}>
@@ -255,7 +255,8 @@
255
255
 
256
256
  .oinBadgeLink {
257
257
  display: inline-block;
258
- transition: opacity var(--durationS, 0.2s) var(--bezierFastoutSlowin, ease-out);
258
+ transition: opacity var(--durationS, 0.2s)
259
+ var(--bezierFastoutSlowin, ease-out);
259
260
  }
260
261
 
261
262
  .oinBadgeLink:hover {
@@ -317,5 +318,3 @@
317
318
  background: #5c636a;
318
319
  box-shadow: 0 2px 6px color-mix(in lab, #6c757d 40%, transparent);
319
320
  }
320
-
321
-
@@ -6,10 +6,8 @@ import { SignOut } from '../actions/signout';
6
6
  import { CaseSidebar } from './cases/case-sidebar';
7
7
  import { NotesSidebar } from './notes/notes-sidebar';
8
8
  import { CaseImport } from './case-import/case-import';
9
- import { HashUtility } from './hash/hash-utility';
10
9
  import { Toast } from '../toast/toast';
11
- import { FileData } from '~/types';
12
- import { ImportResult, ConfirmationImportResult } from '~/types';
10
+ import { FileData, ImportResult, ConfirmationImportResult } from '~/types';
13
11
 
14
12
  interface SidebarProps {
15
13
  user: User;
@@ -64,7 +62,6 @@ export const Sidebar = ({
64
62
  }: SidebarProps) => {
65
63
  const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
66
64
  const [isImportModalOpen, setIsImportModalOpen] = useState(false);
67
- const [isHashModalOpen, setIsHashModalOpen] = useState(false);
68
65
  const [isUploading, setIsUploading] = useState(initialIsUploading);
69
66
  const [toastMessage, setToastMessage] = useState('');
70
67
  const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
@@ -143,10 +140,6 @@ export const Sidebar = ({
143
140
  onClose={() => setIsImportModalOpen(false)}
144
141
  onImportComplete={handleImportComplete}
145
142
  />
146
- <HashUtility
147
- isOpen={isHashModalOpen}
148
- onClose={() => setIsHashModalOpen(false)}
149
- />
150
143
  {showNotes ? (
151
144
  <NotesSidebar
152
145
  currentCase={currentCase}
@@ -193,14 +186,6 @@ export const Sidebar = ({
193
186
  >
194
187
  Import/Clear RO Case
195
188
  </button>
196
- <button
197
- onClick={() => setIsHashModalOpen(true)}
198
- className={styles.hashButton}
199
- disabled={isUploading}
200
- title={isUploading ? 'Cannot open hash utility while uploading files' : undefined}
201
- >
202
- Hash Utility
203
- </button>
204
189
  </div>
205
190
  </>
206
191
  )}
@@ -198,7 +198,7 @@ export const ImageUploadZone = ({
198
198
  }
199
199
  };
200
200
 
201
- const handleFileInputChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
201
+ const handleFileInputChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
202
202
  if (isReadOnly) {
203
203
  return;
204
204
  }
@@ -209,7 +209,7 @@ export const ImageUploadZone = ({
209
209
  // Convert FileList to Array
210
210
  const filesToUpload = Array.from(files);
211
211
  await processFileQueue(filesToUpload);
212
- }, [isReadOnly, currentCase]);
212
+ };
213
213
 
214
214
  const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
215
215
  e.preventDefault();
@@ -234,7 +234,7 @@ export const ImageUploadZone = ({
234
234
  e.stopPropagation();
235
235
  }, []);
236
236
 
237
- const handleDrop = useCallback(async (e: React.DragEvent<HTMLDivElement>) => {
237
+ const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
238
238
  e.preventDefault();
239
239
  e.stopPropagation();
240
240
  setIsDraggingFiles(false);
@@ -249,7 +249,7 @@ export const ImageUploadZone = ({
249
249
  // Convert FileList to Array and process all files
250
250
  const filesToUpload = Array.from(files);
251
251
  await processFileQueue(filesToUpload);
252
- }, [isReadOnly, currentCase]);
252
+ };
253
253
 
254
254
  // If read-only or uploads restricted, show only error message
255
255
  if (isReadOnly || !canUploadNewFile) {
@@ -84,7 +84,7 @@
84
84
  border-radius: 3px;
85
85
  padding: var(--spaceXS) var(--spaceS);
86
86
  cursor: pointer;
87
- font-size: var(--fontSizeXS);
87
+ font-size: var(--fontSizeBodyXS);
88
88
  }
89
89
 
90
90
  .toggleButton:hover {
@@ -157,7 +157,7 @@
157
157
  }
158
158
 
159
159
  .previewLabel {
160
- font-size: var(--fontSizeXS);
160
+ font-size: var(--fontSizeBodyXS);
161
161
  color: var(--foreground);
162
162
  font-weight: 500;
163
163
  }
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import styles from './toolbar-color-selector.module.css';
3
3
 
4
4
  interface ToolbarColorSelectorProps {
@@ -39,11 +39,11 @@ export const ToolbarColorSelector = ({
39
39
  onColorConfirm(tempSelectedColor);
40
40
  };
41
41
 
42
- const handleCancel = () => {
42
+ const handleCancel = useCallback(() => {
43
43
  setTempSelectedColor(selectedColor); // Reset to original color
44
44
  setShowColorWheel(false); // Reset to presets view
45
45
  onCancel();
46
- };
46
+ }, [selectedColor, onCancel]);
47
47
 
48
48
  // Handle keyboard events for escape
49
49
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import { Button } from '../button/button';
3
3
  import { ToolbarColorSelector } from './toolbar-color-selector';
4
4
  import styles from './toolbar.module.css';
@@ -34,18 +34,28 @@ export const Toolbar = ({
34
34
  const [isVisible, setIsVisible] = useState(true);
35
35
  const [showColorSelector, setShowColorSelector] = useState(false);
36
36
 
37
+ const deactivateBoxTool = useCallback(() => {
38
+ setActiveTools(prev => {
39
+ const next = new Set(prev);
40
+ next.delete('box');
41
+ onToolSelect?.('box', false);
42
+ return next;
43
+ });
44
+ setShowColorSelector(false);
45
+ }, [onToolSelect]);
46
+
37
47
  // Automatically deactivate box annotations when notes are opened
38
48
  useEffect(() => {
39
49
  if (isNotesOpen && activeTools.has('box')) {
40
- setActiveTools(prev => {
41
- const next = new Set(prev);
42
- next.delete('box');
43
- onToolSelect?.('box', false);
44
- return next;
45
- });
46
- setShowColorSelector(false);
50
+ const deactivateTimer = window.setTimeout(() => {
51
+ deactivateBoxTool();
52
+ }, 0);
53
+
54
+ return () => {
55
+ window.clearTimeout(deactivateTimer);
56
+ };
47
57
  }
48
- }, [isNotesOpen, activeTools, onToolSelect]);
58
+ }, [isNotesOpen, activeTools, deactivateBoxTool]);
49
59
 
50
60
  const handleToolClick = (toolId: ToolId) => {
51
61
  if (toolId === 'print') {
@@ -7,6 +7,7 @@
7
7
  justify-content: center;
8
8
  align-items: center;
9
9
  z-index: var(--zIndex5);
10
+ cursor: default;
10
11
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
11
12
  }
12
13
 
@@ -16,11 +17,13 @@
16
17
  border-radius: var(--spaceXS);
17
18
  width: 90%;
18
19
  max-width: 600px;
19
- box-shadow: 0 var(--spaceXS) var(--spaceL) color-mix(in lab, var(--black) 10%, transparent);
20
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
21
+ color-mix(in lab, var(--black) 10%, transparent);
20
22
  transition: background-color var(--durationM) var(--bezierFastoutSlowin);
21
23
  overflow: hidden;
22
24
  max-height: 90vh;
23
25
  overflow-y: auto;
26
+ cursor: default;
24
27
  }
25
28
 
26
29
  /* Modal Header */
@@ -317,18 +317,37 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
317
317
  ? `Deleting case ${deletionProgress.currentCaseNumber}...`
318
318
  : 'Preparing account deletion...');
319
319
 
320
+ const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
321
+ if (event.target === event.currentTarget) {
322
+ onClose();
323
+ }
324
+ };
325
+
326
+ const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
327
+ if (event.target !== event.currentTarget) {
328
+ return;
329
+ }
330
+
331
+ if (event.key === 'Enter' || event.key === ' ') {
332
+ event.preventDefault();
333
+ onClose();
334
+ }
335
+ };
336
+
320
337
  return (
321
338
  <div
322
339
  className={styles.modalOverlay}
323
- onClick={onClose}
324
- role="presentation"
340
+ onClick={handleOverlayClick}
341
+ onKeyDown={handleOverlayKeyDown}
342
+ role="button"
343
+ tabIndex={0}
344
+ aria-label="Close delete account dialog"
325
345
  >
326
346
  <div
327
347
  className={styles.modal}
328
348
  role="dialog"
329
349
  aria-modal="true"
330
350
  aria-labelledby="modal-title"
331
- onClick={(e) => e.stopPropagation()}
332
351
  >
333
352
  {/* Header */}
334
353
  <header className={styles.modalHeader}>
@@ -21,7 +21,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
21
21
  const [displayName, setDisplayName] = useState(user?.displayName || '');
22
22
  const [company, setCompany] = useState('');
23
23
  const [email, setEmail] = useState('');
24
- const [permitted, setPermitted] = useState(false); // Default to false for safety - updated after data loads.
25
24
  const [isLoading, setIsLoading] = useState(false);
26
25
  const [isMfaBusy, setIsMfaBusy] = useState(false);
27
26
  const [error, setError] = useState('');
@@ -52,7 +51,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
52
51
  if (userData) {
53
52
  setCompany(userData.company || '');
54
53
  setEmail(userData.email || '');
55
- setPermitted(userData.permitted === true);
56
54
  }
57
55
  } catch (err) {
58
56
  console.error('Failed to load user data:', err);
@@ -1,4 +1,4 @@
1
- import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
1
+ import type { EntryContext } from "@remix-run/cloudflare";
2
2
  import { RemixServer } from "@remix-run/react";
3
3
  import { isbot } from "isbot";
4
4
  import { renderToReadableStream } from "react-dom/server";
@@ -7,8 +7,7 @@ export default async function handleRequest(
7
7
  request: Request,
8
8
  responseStatusCode: number,
9
9
  responseHeaders: Headers,
10
- remixContext: EntryContext,
11
- loadContext: AppLoadContext
10
+ remixContext: EntryContext
12
11
  ) {
13
12
  const body = await renderToReadableStream(
14
13
  <RemixServer context={remixContext} url={request.url} />,
@@ -22,11 +22,15 @@ export const useInactivityTimeout = ({
22
22
  const location = useLocation();
23
23
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
24
24
  const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
25
- const lastActivityRef = useRef<number>(Date.now());
25
+ const lastActivityRef = useRef<number>(0);
26
26
 
27
27
  const isAuthRoute = location.pathname.startsWith('/auth');
28
28
  const shouldEnable = enabled && isAuthRoute;
29
29
 
30
+ useEffect(() => {
31
+ lastActivityRef.current = Date.now();
32
+ }, []);
33
+
30
34
  const clearTimeouts = useCallback(() => {
31
35
  if (timeoutRef.current) {
32
36
  clearTimeout(timeoutRef.current);
package/app/root.tsx CHANGED
@@ -30,11 +30,8 @@ export const links: LinksFunction = () => [
30
30
  rel: "stylesheet",
31
31
  href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
32
32
  },
33
- { rel: 'manifest', href: '/manifest.json' },
34
33
  { rel: 'icon', href: '/favicon.ico' },
35
34
  { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
36
- { rel: 'shortcut_icon', href: '/shortcut.png', type: 'image/png', sizes: '64x64' },
37
- { rel: 'apple-touch-icon', href: '/icon-256.png', sizes: '256x256' },
38
35
  ];
39
36
 
40
37
  type AppTheme = 'dark' | 'light';
@@ -1,16 +1 @@
1
- import { redirect, type LoaderFunctionArgs } from '@remix-run/cloudflare';
2
- import { isMobileOrTabletUserAgent } from '~/utils/device-detection';
3
-
4
- export const loader = async ({ request }: LoaderFunctionArgs) => {
5
- const requestUrl = new URL(request.url);
6
- const search = requestUrl.search ?? '';
7
- const userAgent = request.headers.get('user-agent') ?? '';
8
-
9
- if (isMobileOrTabletUserAgent(userAgent)) {
10
- throw redirect(`/mobile-prevented${search}`);
11
- }
12
-
13
- return null;
14
- };
15
-
16
- export { Login as default, meta } from './auth/login';
1
+ export { Login as default, meta } from './auth/login';
@@ -107,7 +107,7 @@ export const EmailVerification = ({
107
107
  </div>
108
108
 
109
109
  <div className={styles.verificationHints}>
110
- <p className={styles.hint}>Didn't receive the email?</p>
110
+ <p className={styles.hint}>Didn&apos;t receive the email?</p>
111
111
  <ul className={styles.hintList}>
112
112
  <li>Check your spam or junk folder</li>
113
113
  <li>Make sure {user?.email} is correct</li>
@@ -20,7 +20,6 @@ import { MFAVerification } from '~/components/auth/mfa-verification';
20
20
  import { MFAEnrollment } from '~/components/auth/mfa-enrollment';
21
21
  import { Icon } from '~/components/icon/icon';
22
22
  import styles from './login.module.css';
23
- import { baseMeta } from '~/utils/meta';
24
23
  import { Striae } from '~/routes/striae/striae';
25
24
  import { getUserData, createUser } from '~/utils/permissions';
26
25
  import { auditService } from '~/services/audit.service';
@@ -30,10 +29,13 @@ import { buildActionCodeSettings } from '~/utils/auth-action-settings';
30
29
  import { userHasMFA } from '~/utils/mfa';
31
30
 
32
31
  export const meta = () => {
33
- return baseMeta({
34
- title: 'Welcome to Striae',
35
- description: 'Login to your Striae account to access your projects and data',
36
- });
32
+ const titleText = 'Striae | Welcome to Striae';
33
+ const description = 'Login to your Striae account to access your projects and data';
34
+
35
+ return [
36
+ { title: titleText },
37
+ { name: 'description', content: description },
38
+ ];
37
39
  };
38
40
 
39
41
  const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
@@ -1,16 +1,7 @@
1
- import { redirect, type LoaderFunctionArgs } from '@remix-run/cloudflare';
2
- import { isMobileOrTabletUserAgent } from '~/utils/device-detection';
1
+ import { redirect } from '@remix-run/cloudflare';
3
2
 
4
- export const loader = async ({ request }: LoaderFunctionArgs) => {
5
- const requestUrl = new URL(request.url);
6
- const search = requestUrl.search ?? '';
7
- const userAgent = request.headers.get('user-agent') ?? '';
8
-
9
- if (isMobileOrTabletUserAgent(userAgent)) {
10
- throw redirect(`/mobile-prevented${search}`);
11
- }
12
-
13
- throw redirect(`/${search}`);
3
+ export const loader = async () => {
4
+ throw redirect('/');
14
5
  };
15
6
 
16
7
  export { Login as default, meta } from './login';
@@ -125,7 +125,7 @@ export const Striae = ({ user }: StriaePage) => {
125
125
  };
126
126
 
127
127
  checkReadOnlyStatus();
128
- }, [currentCase, user?.uid]);
128
+ }, [currentCase, user]);
129
129
 
130
130
  // Disable box annotation mode when notes sidebar is opened
131
131
  useEffect(() => {
@@ -18,6 +18,20 @@ import { generateWorkflowId } from '../utils/id-generator';
18
18
 
19
19
  const AUDIT_WORKER_URL = paths.audit_worker_url;
20
20
 
21
+ type AnnotationSnapshot = Record<string, unknown> & {
22
+ type?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
23
+ position?: { x: number; y: number };
24
+ size?: { width: number; height: number };
25
+ };
26
+
27
+ const toAnnotationSnapshot = (value: unknown): AnnotationSnapshot | undefined => {
28
+ if (typeof value !== 'object' || value === null) {
29
+ return undefined;
30
+ }
31
+
32
+ return value as AnnotationSnapshot;
33
+ };
34
+
21
35
  /**
22
36
  * Audit Service for ValidationAuditEntry system
23
37
  * Provides comprehensive audit logging throughout the confirmation workflow
@@ -552,7 +566,7 @@ export class AuditService {
552
566
  fileId,
553
567
  originalFileName,
554
568
  fileSize: 0, // File size not available for access events
555
- uploadMethod: accessMethod as any, // Reuse for access method
569
+ uploadMethod: accessMethod,
556
570
  processingTime,
557
571
  sourceLocation: accessReason || 'Image viewer'
558
572
  },
@@ -570,12 +584,14 @@ export class AuditService {
570
584
  user: User,
571
585
  annotationId: string,
572
586
  annotationType: 'measurement' | 'identification' | 'comparison' | 'note' | 'region',
573
- annotationData: any,
587
+ annotationData: unknown,
574
588
  caseNumber: string,
575
589
  tool?: string,
576
590
  imageFileId?: string,
577
591
  originalImageFileName?: string
578
592
  ): Promise<void> {
593
+ const annotationSnapshot = toAnnotationSnapshot(annotationData);
594
+
579
595
  await this.logEvent({
580
596
  userId: user.uid,
581
597
  userEmail: user.email || '',
@@ -591,8 +607,8 @@ export class AuditService {
591
607
  annotationType,
592
608
  annotationData,
593
609
  tool,
594
- canvasPosition: annotationData?.position,
595
- annotationSize: annotationData?.size
610
+ canvasPosition: annotationSnapshot?.position,
611
+ annotationSize: annotationSnapshot?.size
596
612
  },
597
613
  fileDetails: imageFileId || originalImageFileName ? {
598
614
  fileId: imageFileId,
@@ -610,13 +626,15 @@ export class AuditService {
610
626
  public async logAnnotationEdit(
611
627
  user: User,
612
628
  annotationId: string,
613
- previousValue: any,
614
- newValue: any,
629
+ previousValue: unknown,
630
+ newValue: unknown,
615
631
  caseNumber: string,
616
632
  tool?: string,
617
633
  imageFileId?: string,
618
634
  originalImageFileName?: string
619
635
  ): Promise<void> {
636
+ const newValueSnapshot = toAnnotationSnapshot(newValue);
637
+
620
638
  await this.logEvent({
621
639
  userId: user.uid,
622
640
  userEmail: user.email || '',
@@ -629,7 +647,7 @@ export class AuditService {
629
647
  workflowPhase: 'casework',
630
648
  annotationDetails: {
631
649
  annotationId,
632
- annotationType: newValue?.type,
650
+ annotationType: newValueSnapshot?.type,
633
651
  annotationData: newValue,
634
652
  previousValue,
635
653
  tool
@@ -650,12 +668,14 @@ export class AuditService {
650
668
  public async logAnnotationDelete(
651
669
  user: User,
652
670
  annotationId: string,
653
- annotationData: any,
671
+ annotationData: unknown,
654
672
  caseNumber: string,
655
673
  deleteReason?: string,
656
674
  imageFileId?: string,
657
675
  originalImageFileName?: string
658
676
  ): Promise<void> {
677
+ const annotationSnapshot = toAnnotationSnapshot(annotationData);
678
+
659
679
  await this.logEvent({
660
680
  userId: user.uid,
661
681
  userEmail: user.email || '',
@@ -668,7 +688,7 @@ export class AuditService {
668
688
  workflowPhase: 'casework',
669
689
  annotationDetails: {
670
690
  annotationId,
671
- annotationType: annotationData?.type,
691
+ annotationType: annotationSnapshot?.type,
672
692
  annotationData,
673
693
  tool: deleteReason
674
694
  },
package/app/tailwind.css CHANGED
@@ -43,8 +43,23 @@
43
43
  display: block;
44
44
  }
45
45
 
46
+ ul,
47
+ ol {
48
+ padding-left: var(--spaceL);
49
+ margin: 0;
50
+ list-style-position: outside;
51
+ }
52
+
46
53
  ul {
47
- padding: 0;
54
+ list-style-type: disc;
55
+ }
56
+
57
+ ol {
58
+ list-style-type: decimal;
59
+ }
60
+
61
+ ol li::marker {
62
+ content: counter(list-item) ") ";
48
63
  }
49
64
 
50
65
  html,
@@ -211,7 +211,7 @@ export interface FileAuditDetails {
211
211
  originalFileName?: string;
212
212
  fileSize: number;
213
213
  mimeType?: string;
214
- uploadMethod?: 'drag-drop' | 'file-picker' | 'api' | 'import';
214
+ uploadMethod?: 'drag-drop' | 'file-picker' | 'api' | 'import' | 'direct-url' | 'signed-url' | 'download';
215
215
  processingTime?: number;
216
216
  thumbnailGenerated?: boolean;
217
217
  deleteReason?: string;
@@ -224,10 +224,10 @@ export interface FileAuditDetails {
224
224
  export interface AnnotationAuditDetails {
225
225
  annotationId?: string;
226
226
  annotationType?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
227
- annotationData?: any; // The actual annotation data structure
227
+ annotationData?: unknown; // The actual annotation data structure
228
228
  canvasPosition?: { x: number; y: number };
229
229
  annotationSize?: { width: number; height: number };
230
- previousValue?: any; // For edit operations
230
+ previousValue?: unknown; // For edit operations
231
231
  tool?: string; // Which tool was used to create/edit
232
232
  }
233
233
 
package/app/types/case.ts CHANGED
@@ -82,7 +82,7 @@ export interface CaseConfirmations {
82
82
  export interface CaseDataWithConfirmations {
83
83
  createdAt: string;
84
84
  caseNumber: string;
85
- files: any[];
85
+ files: FileData[];
86
86
  isReadOnly?: boolean;
87
87
  importedAt?: string;
88
88
  originalImageIds?: { [originalId: string]: string };