diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 000000000..cd779cd72
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,55 @@
+module.exports = {
+ extends: [
+ 'react-app',
+ 'react-app/jest'
+ ],
+ rules: {
+ // Custom rules to prevent dangerous file.name as ID patterns
+ 'no-file-name-as-id': 'error',
+ 'prefer-file-with-id': 'warn'
+ },
+ overrides: [
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ rules: {
+ // Prevent file.name being used where FileId is expected
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: 'MemberExpression[object.name="file"][property.name="name"]',
+ message: 'Avoid using file.name directly. Use FileWithId.fileId or safeGetFileId() instead to prevent ID collisions.'
+ },
+ {
+ selector: 'CallExpression[callee.name="createOperation"] > ArrayExpression > CallExpression[callee.property.name="map"] > ArrowFunctionExpression > MemberExpression[object.name="f"][property.name="name"]',
+ message: 'Dangerous pattern: Using file.name as ID in createOperation. Use FileWithId.fileId instead.'
+ },
+ {
+ selector: 'ArrayExpression[elements.length>0] CallExpression[callee.property.name="map"] > ArrowFunctionExpression > MemberExpression[property.name="name"]',
+ message: 'Potential file.name as ID usage detected. Ensure proper FileId usage instead of file.name.'
+ }
+ ]
+ }
+ }
+ ],
+ settings: {
+ // Custom settings for our file ID validation
+ 'file-id-validation': {
+ // Functions that should only accept FileId, not strings
+ 'file-id-only-functions': [
+ 'recordOperation',
+ 'markOperationApplied',
+ 'markOperationFailed',
+ 'removeFiles',
+ 'updateFileRecord',
+ 'pinFile',
+ 'unpinFile'
+ ],
+ // Functions that should accept FileWithId instead of File
+ 'file-with-id-functions': [
+ 'createOperation',
+ 'executeOperation',
+ 'isFilePinned'
+ ]
+ }
+ }
+};
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index ef4d663f6..9b2d23698 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -13,6 +13,9 @@ import "./styles/tailwind.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
+// Import file ID safety validators (development only)
+import "./utils/fileIdSafety";
+
// Loading component for i18next suspense
const LoadingFallback = () => (
0) {
- // Record upload operations for PDF files
- for (const file of allExtractedFiles) {
- const operationId = `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- const operation: FileOperation = {
- id: operationId,
- type: 'upload',
- timestamp: Date.now(),
- fileIds: [file.name],
- status: 'pending',
- metadata: {
- originalFileName: file.name,
- fileSize: file.size,
- parameters: {
- uploadMethod: 'drag-drop'
- }
- }
- };
- }
-
// Add files to context (they will be processed automatically)
await addFiles(allExtractedFiles);
setStatus(`Added ${allExtractedFiles.length} files`);
diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx
index 169e19f88..ea0e70818 100644
--- a/frontend/src/components/shared/FileGrid.tsx
+++ b/frontend/src/components/shared/FileGrid.tsx
@@ -123,8 +123,13 @@ const FileGrid = ({
style={{ overflowY: "auto", width: "100%" }}
>
{displayFiles.map((item, idx) => {
- const fileId = item.record?.id || item.file.name;
- const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
+ // Use record ID if available, otherwise throw error for missing FileRecord
+ if (!item.record?.id) {
+ console.error('FileGrid: File missing FileRecord with proper ID:', item.file.name);
+ return null; // Skip rendering files without proper IDs
+ }
+ const fileId = item.record.id;
+ const originalIdx = files.findIndex(f => f.record?.id === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return (
=> {
+ const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string }): Promise => {
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
// Persist to IndexedDB if enabled
@@ -87,56 +89,40 @@ function FileContextInner({
}));
}
- return addedFilesWithIds.map(({ file }) => file);
+ // Convert to FileWithId objects
+ return addedFilesWithIds.map(({ file, id }) => createFileWithId(file, id));
}, [indexedDB, enablePersistence]);
- const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
+ const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
- return result.map(({ file }) => file);
+ // Convert to FileWithId objects
+ return result.map(({ file, id }) => createFileWithId(file, id));
}, []);
- const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise => {
+ const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>): Promise => {
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
- return result.map(({ file }) => file);
+ // Convert to FileWithId objects
+ return result.map(({ file, id }) => createFileWithId(file, id));
}, []);
// Action creators
const baseActions = useMemo(() => createFileActions(dispatch), []);
// Helper functions for pinned files
- const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => {
- return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch);
+ const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => {
+ const result = await consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch);
+ // Convert results to FileWithId objects
+ return result.map(({ file, id }) => createFileWithId(file, id));
}, []);
- // Helper to find FileId from File object
- const findFileId = useCallback((file: File): FileId | undefined => {
- return Object.keys(stateRef.current.files.byId).find(id => {
- const storedFile = filesRef.current.get(id);
- return storedFile &&
- storedFile.name === file.name &&
- storedFile.size === file.size &&
- storedFile.lastModified === file.lastModified;
- });
- }, []);
+ // File pinning functions - now use FileWithId directly
+ const pinFileWrapper = useCallback((file: FileWithId) => {
+ baseActions.pinFile(file.fileId);
+ }, [baseActions]);
- // File-to-ID wrapper functions for pinning
- const pinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.pinFile(fileId);
- } else {
- console.warn('File not found for pinning:', file.name);
- }
- }, [baseActions, findFileId]);
-
- const unpinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.unpinFile(fileId);
- } else {
- console.warn('File not found for unpinning:', file.name);
- }
- }, [baseActions, findFileId]);
+ const unpinFileWrapper = useCallback((file: FileWithId) => {
+ baseActions.unpinFile(file.fileId);
+ }, [baseActions]);
// Complete actions object
const actions = useMemo(() => ({
diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts
index c74fa66a5..a623e1901 100644
--- a/frontend/src/contexts/file/fileActions.ts
+++ b/frontend/src/contexts/file/fileActions.ts
@@ -326,11 +326,11 @@ export async function consumeFiles(
stateRef: React.MutableRefObject,
filesRef: React.MutableRefObject