From 68d280e8f54d6635978d7b8a7a457bcad8dcb06d Mon Sep 17 00:00:00 2001 From: Tanya Date: Tue, 30 Dec 2025 15:04:32 -0500 Subject: [PATCH] feat: Add new analysis documents and update installation scripts for backend integration This commit introduces several new analysis documents, including Auto-Match Load Performance Analysis, Folder Picker Analysis, Monorepo Migration Summary, and various performance analysis documents. Additionally, the installation scripts are updated to reflect changes in backend service paths, ensuring proper integration with the new backend structure. These enhancements provide better documentation and streamline the setup process for users. --- AUTOMATCH_LOAD_ANALYSIS.md | 174 +++ CONFIDENCE_CALIBRATION_SUMMARY.md | 1 + FOLDER_PICKER_ANALYSIS.md | 233 +++ MONOREPO_MIGRATION.md | 125 ++ README.md | 298 ++-- {frontend => admin-frontend}/.eslintrc.cjs | 4 + {frontend => admin-frontend}/README.md | 0 {frontend => admin-frontend}/index.html | 0 .../package-lock.json | 1262 +++++++++-------- {frontend => admin-frontend}/package.json | 2 +- .../postcss.config.js | 0 {frontend => admin-frontend}/src/App.tsx | 0 {frontend => admin-frontend}/src/api/auth.ts | 0 .../src/api/authUsers.ts | 0 .../src/api/client.ts | 22 +- {frontend => admin-frontend}/src/api/faces.ts | 0 {frontend => admin-frontend}/src/api/jobs.ts | 1 + .../src/api/pendingIdentifications.ts | 0 .../src/api/pendingLinkages.ts | 0 .../src/api/pendingPhotos.ts | 0 .../src/api/people.ts | 0 .../src/api/photos.ts | 1 + .../src/api/reportedPhotos.ts | 0 .../src/api/rolePermissions.ts | 4 + {frontend => admin-frontend}/src/api/tags.ts | 0 {frontend => admin-frontend}/src/api/users.ts | 0 .../src/api/videos.ts | 4 + .../src/components/AdminRoute.tsx | 0 .../src/components/Layout.tsx | 0 .../src/components/PasswordChangeModal.tsx | 0 .../src/components/PhotoViewer.tsx | 0 .../src/context/AuthContext.tsx | 0 .../src/context/DeveloperModeContext.tsx | 0 .../src/hooks/useAuth.ts | 0 .../src/hooks/useInactivityTimeout.ts | 0 {frontend => admin-frontend}/src/index.css | 0 {frontend => admin-frontend}/src/main.tsx | 0 .../src/pages/ApproveIdentified.tsx | 12 +- .../src/pages/AutoMatch.tsx | 0 .../src/pages/Dashboard.tsx | 0 .../src/pages/FacesMaintenance.tsx | 0 .../src/pages/Help.tsx | 0 .../src/pages/Identify.tsx | 0 .../src/pages/Login.tsx | 0 .../src/pages/ManagePhotos.tsx | 0 .../src/pages/ManageUsers.tsx | 0 .../src/pages/Modify.tsx | 0 .../src/pages/PendingPhotos.tsx | 0 .../src/pages/Process.tsx | 0 .../src/pages/ReportedPhotos.tsx | 0 .../src/pages/Scan.tsx | 0 .../src/pages/Search.tsx | 5 +- .../src/pages/Settings.tsx | 0 .../src/pages/Tags.tsx | 0 .../src/pages/UserTaggedPhotos.tsx | 0 .../src/vite-env.d.ts | 0 .../tailwind.config.js | 0 {frontend => admin-frontend}/tsconfig.json | 0 .../tsconfig.node.json | 0 {frontend => admin-frontend}/vite.config.ts | 0 backend/__init__.py | 0 {src/web => backend}/api/__init__.py | 0 {src/web => backend}/api/auth.py | 23 +- {src/web => backend}/api/auth_users.py | 10 +- {src/web => backend}/api/faces.py | 22 +- {src/web => backend}/api/health.py | 0 {src/web => backend}/api/jobs.py | 2 +- {src/web => backend}/api/metrics.py | 0 .../api/pending_identifications.py | 10 +- {src/web => backend}/api/pending_linkages.py | 6 +- {src/web => backend}/api/pending_photos.py | 10 +- {src/web => backend}/api/people.py | 16 +- {src/web => backend}/api/photos.py | 36 +- {src/web => backend}/api/reported_photos.py | 6 +- {src/web => backend}/api/role_permissions.py | 16 +- {src/web => backend}/api/tags.py | 8 +- {src/web => backend}/api/users.py | 14 +- {src/web => backend}/api/version.py | 2 +- {src/web => backend}/api/videos.py | 16 +- {src/web => backend}/app.py | 264 +++- {src/web => backend}/config.py | 0 .../constants/role_features.py | 2 +- {src/web => backend}/constants/roles.py | 0 {src/web => backend}/db/__init__.py | 0 {src/web => backend}/db/base.py | 4 +- {src/web => backend}/db/models.py | 2 +- {src/web => backend}/db/session.py | 0 {src/web => backend}/schemas/__init__.py | 0 {src/web => backend}/schemas/auth.py | 2 +- {src/web => backend}/schemas/auth_users.py | 0 {src/web => backend}/schemas/faces.py | 0 {src/web => backend}/schemas/jobs.py | 0 {src/web => backend}/schemas/people.py | 0 {src/web => backend}/schemas/photos.py | 0 .../schemas/role_permissions.py | 8 +- {src/web => backend}/schemas/search.py | 0 {src/web => backend}/schemas/tags.py | 0 {src/web => backend}/schemas/users.py | 2 +- {src/web => backend}/schemas/videos.py | 4 + {src/web => backend}/services/__init__.py | 0 {src/web => backend}/services/face_service.py | 10 +- .../web => backend}/services/photo_service.py | 4 +- .../services/role_permissions.py | 6 +- .../services/search_service.py | 4 +- {src/web => backend}/services/tag_service.py | 2 +- {src/web => backend}/services/tasks.py | 6 +- .../services/thumbnail_service.py | 0 .../web => backend}/services/video_service.py | 2 +- {src/web => backend}/settings.py | 0 {src/web => backend}/utils/__init__.py | 0 {src/web => backend}/utils/password.py | 0 {src/web => backend}/worker.py | 2 +- deploy/docker-compose.yml | 4 +- docs/CLIENT_DEPLOYMENT_QUESTIONS.md | 219 +++ docs/CLIENT_NETWORK_TESTING_INFO_for_BOTH.md | 505 +++++++ docs/RESOURCE_REQUIREMENTS.md | 401 ++++++ docs/TAG_PHOTOS_PERFORMANCE_ANALYSIS.md | 234 +++ docs/VIDEO_PERSON_IDENTIFICATION_ANALYSIS.md | 642 +++++++++ docs/VIDEO_SUPPORT_ANALYSIS.md | 479 +++++++ frontend/.vite/deps/_metadata.json | 8 + frontend/.vite/deps/package.json | 3 + install.sh | 81 +- package.json | 21 + run_api_with_worker.sh | 48 +- run_worker.sh | 2 +- scripts/check_database_tables.py | 135 ++ scripts/check_identified_poses_web.py | 4 +- scripts/check_two_faces_pose.py | 4 +- scripts/debug_pose_classification.py | 4 +- scripts/diagnose_frontend_issues.py | 160 +++ scripts/drop_all_tables_web.py | 4 +- scripts/drop_auth_database.sh | 15 + scripts/fix_admin_password.py | 52 + scripts/migrate_sqlite_to_postgresql.py | 264 ++++ scripts/recreate_tables_web.py | 4 +- scripts/setup_postgresql.sh | 10 +- scripts/show_db_tables.py | 6 +- scripts/update_reported_photo_status.py | 2 +- start_backend.sh | 30 + stop_backend.sh | 24 + 140 files changed, 5101 insertions(+), 933 deletions(-) create mode 100644 AUTOMATCH_LOAD_ANALYSIS.md create mode 100644 FOLDER_PICKER_ANALYSIS.md create mode 100644 MONOREPO_MIGRATION.md rename {frontend => admin-frontend}/.eslintrc.cjs (99%) rename {frontend => admin-frontend}/README.md (100%) rename {frontend => admin-frontend}/index.html (100%) rename {frontend => admin-frontend}/package-lock.json (88%) rename {frontend => admin-frontend}/package.json (97%) rename {frontend => admin-frontend}/postcss.config.js (100%) rename {frontend => admin-frontend}/src/App.tsx (100%) rename {frontend => admin-frontend}/src/api/auth.ts (100%) rename {frontend => admin-frontend}/src/api/authUsers.ts (100%) rename {frontend => admin-frontend}/src/api/client.ts (58%) rename {frontend => admin-frontend}/src/api/faces.ts (100%) rename {frontend => admin-frontend}/src/api/jobs.ts (91%) rename {frontend => admin-frontend}/src/api/pendingIdentifications.ts (100%) rename {frontend => admin-frontend}/src/api/pendingLinkages.ts (100%) rename {frontend => admin-frontend}/src/api/pendingPhotos.ts (100%) rename {frontend => admin-frontend}/src/api/people.ts (100%) rename {frontend => admin-frontend}/src/api/photos.ts (97%) rename {frontend => admin-frontend}/src/api/reportedPhotos.ts (100%) rename {frontend => admin-frontend}/src/api/rolePermissions.ts (99%) rename {frontend => admin-frontend}/src/api/tags.ts (100%) rename {frontend => admin-frontend}/src/api/users.ts (100%) rename {frontend => admin-frontend}/src/api/videos.ts (99%) rename {frontend => admin-frontend}/src/components/AdminRoute.tsx (100%) rename {frontend => admin-frontend}/src/components/Layout.tsx (100%) rename {frontend => admin-frontend}/src/components/PasswordChangeModal.tsx (100%) rename {frontend => admin-frontend}/src/components/PhotoViewer.tsx (100%) rename {frontend => admin-frontend}/src/context/AuthContext.tsx (100%) rename {frontend => admin-frontend}/src/context/DeveloperModeContext.tsx (100%) rename {frontend => admin-frontend}/src/hooks/useAuth.ts (100%) rename {frontend => admin-frontend}/src/hooks/useInactivityTimeout.ts (100%) rename {frontend => admin-frontend}/src/index.css (100%) rename {frontend => admin-frontend}/src/main.tsx (100%) rename {frontend => admin-frontend}/src/pages/ApproveIdentified.tsx (97%) rename {frontend => admin-frontend}/src/pages/AutoMatch.tsx (100%) rename {frontend => admin-frontend}/src/pages/Dashboard.tsx (100%) rename {frontend => admin-frontend}/src/pages/FacesMaintenance.tsx (100%) rename {frontend => admin-frontend}/src/pages/Help.tsx (100%) rename {frontend => admin-frontend}/src/pages/Identify.tsx (100%) rename {frontend => admin-frontend}/src/pages/Login.tsx (100%) rename {frontend => admin-frontend}/src/pages/ManagePhotos.tsx (100%) rename {frontend => admin-frontend}/src/pages/ManageUsers.tsx (100%) rename {frontend => admin-frontend}/src/pages/Modify.tsx (100%) rename {frontend => admin-frontend}/src/pages/PendingPhotos.tsx (100%) rename {frontend => admin-frontend}/src/pages/Process.tsx (100%) rename {frontend => admin-frontend}/src/pages/ReportedPhotos.tsx (100%) rename {frontend => admin-frontend}/src/pages/Scan.tsx (100%) rename {frontend => admin-frontend}/src/pages/Search.tsx (99%) rename {frontend => admin-frontend}/src/pages/Settings.tsx (100%) rename {frontend => admin-frontend}/src/pages/Tags.tsx (100%) rename {frontend => admin-frontend}/src/pages/UserTaggedPhotos.tsx (100%) rename {frontend => admin-frontend}/src/vite-env.d.ts (100%) rename {frontend => admin-frontend}/tailwind.config.js (100%) rename {frontend => admin-frontend}/tsconfig.json (100%) rename {frontend => admin-frontend}/tsconfig.node.json (100%) rename {frontend => admin-frontend}/vite.config.ts (100%) create mode 100644 backend/__init__.py rename {src/web => backend}/api/__init__.py (100%) rename {src/web => backend}/api/auth.py (95%) rename {src/web => backend}/api/auth_users.py (99%) rename {src/web => backend}/api/faces.py (98%) rename {src/web => backend}/api/health.py (100%) rename {src/web => backend}/api/jobs.py (99%) rename {src/web => backend}/api/metrics.py (100%) rename {src/web => backend}/api/pending_identifications.py (98%) rename {src/web => backend}/api/pending_linkages.py (98%) rename {src/web => backend}/api/pending_photos.py (98%) rename {src/web => backend}/api/people.py (96%) rename {src/web => backend}/api/photos.py (97%) rename {src/web => backend}/api/reported_photos.py (98%) rename {src/web => backend}/api/role_permissions.py (87%) rename {src/web => backend}/api/tags.py (97%) rename {src/web => backend}/api/users.py (98%) rename {src/web => backend}/api/version.py (84%) rename {src/web => backend}/api/videos.py (97%) rename {src/web => backend}/app.py (70%) rename {src/web => backend}/config.py (100%) rename {src/web => backend}/constants/role_features.py (97%) rename {src/web => backend}/constants/roles.py (100%) rename {src/web => backend}/db/__init__.py (100%) rename {src/web => backend}/db/base.py (58%) rename {src/web => backend}/db/models.py (99%) rename {src/web => backend}/db/session.py (100%) rename {src/web => backend}/schemas/__init__.py (100%) rename {src/web => backend}/schemas/auth.py (95%) rename {src/web => backend}/schemas/auth_users.py (100%) rename {src/web => backend}/schemas/faces.py (100%) rename {src/web => backend}/schemas/jobs.py (100%) rename {src/web => backend}/schemas/people.py (100%) rename {src/web => backend}/schemas/photos.py (100%) rename {src/web => backend}/schemas/role_permissions.py (90%) rename {src/web => backend}/schemas/search.py (100%) rename {src/web => backend}/schemas/tags.py (100%) rename {src/web => backend}/schemas/users.py (97%) rename {src/web => backend}/schemas/videos.py (99%) rename {src/web => backend}/services/__init__.py (100%) rename {src/web => backend}/services/face_service.py (99%) rename {src/web => backend}/services/photo_service.py (99%) rename {src/web => backend}/services/role_permissions.py (93%) rename {src/web => backend}/services/search_service.py (99%) rename {src/web => backend}/services/tag_service.py (99%) rename {src/web => backend}/services/tasks.py (98%) rename {src/web => backend}/services/thumbnail_service.py (100%) rename {src/web => backend}/services/video_service.py (99%) rename {src/web => backend}/settings.py (100%) rename {src/web => backend}/utils/__init__.py (100%) rename {src/web => backend}/utils/password.py (100%) rename {src/web => backend}/worker.py (95%) create mode 100644 docs/CLIENT_DEPLOYMENT_QUESTIONS.md create mode 100644 docs/CLIENT_NETWORK_TESTING_INFO_for_BOTH.md create mode 100644 docs/RESOURCE_REQUIREMENTS.md create mode 100644 docs/TAG_PHOTOS_PERFORMANCE_ANALYSIS.md create mode 100644 docs/VIDEO_PERSON_IDENTIFICATION_ANALYSIS.md create mode 100644 docs/VIDEO_SUPPORT_ANALYSIS.md create mode 100644 frontend/.vite/deps/_metadata.json create mode 100644 frontend/.vite/deps/package.json create mode 100644 package.json create mode 100644 scripts/check_database_tables.py create mode 100644 scripts/diagnose_frontend_issues.py create mode 100755 scripts/drop_auth_database.sh create mode 100644 scripts/fix_admin_password.py create mode 100644 scripts/migrate_sqlite_to_postgresql.py create mode 100755 start_backend.sh create mode 100755 stop_backend.sh diff --git a/AUTOMATCH_LOAD_ANALYSIS.md b/AUTOMATCH_LOAD_ANALYSIS.md new file mode 100644 index 0000000..689344f --- /dev/null +++ b/AUTOMATCH_LOAD_ANALYSIS.md @@ -0,0 +1,174 @@ +# Auto-Match Load Performance Analysis + +## Summary +Auto-Match page loads significantly slower than Identify page because it lacks the performance optimizations that Identify uses. Auto-Match always fetches all data upfront with no caching, while Identify uses sessionStorage caching and lazy loading. + +## Identify Page Optimizations (Current) + +### 1. **SessionStorage Caching** +- **State Caching**: Caches faces, current index, similar faces, and form data in sessionStorage +- **Settings Caching**: Caches filter settings (pageSize, minQuality, sortBy, etc.) +- **Restoration**: On mount, restores cached state instead of making API calls +- **Implementation**: + - `STATE_KEY = 'identify_state'` - stores faces, currentIdx, similar, faceFormData, selectedSimilar + - `SETTINGS_KEY = 'identify_settings'` - stores filter settings + - Only loads fresh data if no cached state exists + +### 2. **Lazy Loading** +- **Similar Faces**: Only loads similar faces when: + - `compareEnabled` is true + - Current face changes + - Not loaded during initial page load +- **Images**: Uses lazy loading for similar face images (`loading="lazy"`) + +### 3. **Image Preloading** +- Preloads next/previous face images in background +- Uses `new Image()` to preload without blocking UI +- Delayed by 100ms to avoid blocking current image load + +### 4. **Batch Operations** +- Uses `batchSimilarity` endpoint for unique faces filtering +- Single API call instead of multiple individual calls + +### 5. **Progressive State Management** +- Uses refs to track restoration state +- Prevents unnecessary reloads during state restoration +- Only triggers API calls when actually needed + +## Auto-Match Page (Current - No Optimizations) + +### 1. **No Caching** +- **No sessionStorage**: Always makes fresh API calls on mount +- **No state restoration**: Always starts from scratch +- **No settings persistence**: Tolerance and other settings reset on page reload + +### 2. **Eager Loading** +- **All Data Upfront**: Loads ALL people and ALL matches in single API call +- **No Lazy Loading**: All match data loaded even if user never views it +- **No Progressive Loading**: Everything must be loaded before UI is usable + +### 3. **No Image Preloading** +- Images load on-demand as user navigates +- No preloading of next/previous person images + +### 4. **Large API Response** +- Backend returns complete dataset: + - All identified people + - All matches for each person + - All face metadata (photo info, locations, quality scores, etc.) +- Response size can be very large (hundreds of KB to MB) depending on: + - Number of identified people + - Number of matches per person + - Amount of metadata per match + +### 5. **Backend Processing** +The `find_auto_match_matches` function: +- Queries all identified faces (one per person, quality >= 0.3) +- For EACH person, calls `find_similar_faces` to find matches +- This means N database queries (where N = number of people) +- All processing happens synchronously before response is sent + +## Performance Comparison + +### Identify Page Load Flow +``` +1. Check sessionStorage for cached state +2. If cached: Restore state (instant, no API call) +3. If not cached: Load faces (paginated, ~50 faces) +4. Load similar faces only when face changes (lazy) +5. Preload next/previous images (background) +``` + +### Auto-Match Page Load Flow +``` +1. Always call API (no cache check) +2. Backend processes ALL people: + - Query all identified faces + - For each person: query similar faces + - Build complete response with all matches +3. Wait for complete response (can be large) +4. Render all data at once +``` + +## Key Differences + +| Feature | Identify | Auto-Match | +|---------|----------|------------| +| **Caching** | ✅ sessionStorage | ❌ None | +| **State Restoration** | ✅ Yes | ❌ No | +| **Lazy Loading** | ✅ Similar faces only | ❌ All data upfront | +| **Image Preloading** | ✅ Next/prev faces | ❌ None | +| **Pagination** | ✅ Yes (page_size) | ❌ No (all at once) | +| **Progressive Loading** | ✅ Yes | ❌ No | +| **API Call Size** | Small (paginated) | Large (all data) | +| **Backend Queries** | 1-2 queries | N+1 queries (N = people) | + +## Why Auto-Match is Slower + +1. **No Caching**: Every page load requires full API call +2. **Large Response**: All people + all matches in single response +3. **N+1 Query Problem**: Backend makes one query per person to find matches +4. **Synchronous Processing**: All processing happens before response +5. **No Lazy Loading**: All match data loaded even if never viewed + +## Potential Optimizations for Auto-Match + +### 1. **Add SessionStorage Caching** (High Impact) +- Cache people list and matches in sessionStorage +- Restore on mount instead of API call +- Similar to Identify page approach + +### 2. **Lazy Load Matches** (High Impact) +- Load people list first +- Load matches for current person only +- Load matches for next person in background +- Similar to how Identify loads similar faces + +### 3. **Pagination** (Medium Impact) +- Paginate people list (e.g., 20 people per page) +- Load matches only for visible people +- Reduces initial response size + +### 4. **Backend Optimization** (High Impact) +- Batch similarity queries instead of N+1 pattern +- Use `calculate_batch_similarities` for all people at once +- Cache results if tolerance hasn't changed + +### 5. **Image Preloading** (Low Impact) +- Preload reference face images for next/previous people +- Preload match images for current person + +### 6. **Progressive Rendering** (Medium Impact) +- Show people list immediately +- Load matches progressively as user navigates +- Show loading indicators for matches + +## Code Locations + +### Identify Page +- **Frontend**: `frontend/src/pages/Identify.tsx` + - Lines 42-45: SessionStorage keys + - Lines 272-347: State restoration logic + - Lines 349-399: State saving logic + - Lines 496-527: Image preloading + - Lines 258-270: Lazy loading of similar faces + +### Auto-Match Page +- **Frontend**: `frontend/src/pages/AutoMatch.tsx` + - Lines 35-71: `loadAutoMatch` function (always calls API) + - Lines 74-77: Auto-load on mount (no cache check) + +### Backend +- **API Endpoint**: `src/web/api/faces.py` (lines 539-702) +- **Service Function**: `src/web/services/face_service.py` (lines 1736-1846) + - `find_auto_match_matches`: Processes all people synchronously + +## Recommendations + +1. **Immediate**: Add sessionStorage caching (similar to Identify) +2. **High Priority**: Implement lazy loading of matches +3. **Medium Priority**: Optimize backend to use batch queries +4. **Low Priority**: Add image preloading + +The biggest win would be adding sessionStorage caching, which would make subsequent page loads instant (like Identify). + diff --git a/CONFIDENCE_CALIBRATION_SUMMARY.md b/CONFIDENCE_CALIBRATION_SUMMARY.md index e7f36ac..a83e7d0 100644 --- a/CONFIDENCE_CALIBRATION_SUMMARY.md +++ b/CONFIDENCE_CALIBRATION_SUMMARY.md @@ -86,3 +86,4 @@ This implementation provides a foundation for more sophisticated calibration met + diff --git a/FOLDER_PICKER_ANALYSIS.md b/FOLDER_PICKER_ANALYSIS.md new file mode 100644 index 0000000..f0a9e36 --- /dev/null +++ b/FOLDER_PICKER_ANALYSIS.md @@ -0,0 +1,233 @@ +# Folder Picker Analysis - Getting Full Paths + +## Problem +Browsers don't expose full file system paths for security reasons. Current implementation only gets folder names, not full absolute paths. + +## Current Limitations + +### Browser-Based Solutions (Current) +1. **File System Access API** (`showDirectoryPicker`) + - ✅ No confirmation dialog + - ❌ Only returns folder name, not full path + - ❌ Only works in Chrome 86+, Edge 86+, Opera 72+ + +2. **webkitdirectory input** + - ✅ Works in all browsers + - ❌ Shows security confirmation dialog + - ❌ Only returns relative paths, not absolute paths + +## Alternative Solutions + +### ✅ **Option 1: Backend API with Tkinter (RECOMMENDED)** + +**How it works:** +- Frontend calls backend API endpoint +- Backend uses `tkinter.filedialog.askdirectory()` to show native folder picker +- Backend returns full absolute path to frontend +- Frontend populates the path input + +**Pros:** +- ✅ Returns full absolute path +- ✅ Native OS dialog (looks native on Windows/Linux/macOS) +- ✅ No browser security restrictions +- ✅ tkinter already used in project +- ✅ Cross-platform support +- ✅ No confirmation dialogs + +**Cons:** +- ⚠️ Requires backend to be running on same machine as user +- ⚠️ Backend needs GUI access (tkinter requires display) +- ⚠️ May need X11 forwarding for remote servers + +**Implementation:** +```python +# Backend API endpoint +@router.post("/browse-folder") +def browse_folder() -> dict: + """Open native folder picker and return selected path.""" + import tkinter as tk + from tkinter import filedialog + + # Create root window (hidden) + root = tk.Tk() + root.withdraw() # Hide main window + root.attributes('-topmost', True) # Bring to front + + # Show folder picker + folder_path = filedialog.askdirectory( + title="Select folder to scan", + mustexist=True + ) + + root.destroy() + + if folder_path: + return {"path": folder_path, "success": True} + else: + return {"path": "", "success": False, "message": "No folder selected"} +``` + +```typescript +// Frontend API call +const browseFolder = async (): Promise => { + const { data } = await apiClient.post<{path: string, success: boolean}>( + '/api/v1/photos/browse-folder' + ) + return data.success ? data.path : null +} +``` + +--- + +### **Option 2: Backend API with PyQt/PySide** + +**How it works:** +- Similar to Option 1, but uses PyQt/PySide instead of tkinter +- More modern UI, but requires additional dependency + +**Pros:** +- ✅ Returns full absolute path +- ✅ More modern-looking dialogs +- ✅ Better customization options + +**Cons:** +- ❌ Requires additional dependency (PyQt5/PyQt6/PySide2/PySide6) +- ❌ Larger package size +- ❌ Same GUI access requirements as tkinter + +--- + +### **Option 3: Backend API with Platform-Specific Tools** + +**How it works:** +- Use platform-specific command-line tools to open folder pickers +- Windows: PowerShell script +- Linux: `zenity`, `kdialog`, or `yad` +- macOS: AppleScript + +**Pros:** +- ✅ Returns full absolute path +- ✅ No GUI framework required +- ✅ Works on headless servers with X11 forwarding + +**Cons:** +- ❌ Platform-specific code required +- ❌ Requires external tools to be installed +- ❌ More complex implementation +- ❌ Less consistent UI across platforms + +**Example (Linux with zenity):** +```python +import subprocess +import platform + +def browse_folder_zenity(): + result = subprocess.run( + ['zenity', '--file-selection', '--directory'], + capture_output=True, + text=True + ) + return result.stdout.strip() if result.returncode == 0 else None +``` + +--- + +### **Option 4: Electron App (Not Applicable)** + +**How it works:** +- Convert web app to Electron app +- Use Electron's `dialog.showOpenDialog()` API + +**Pros:** +- ✅ Returns full absolute path +- ✅ Native OS dialogs +- ✅ No browser restrictions + +**Cons:** +- ❌ Requires complete app restructuring +- ❌ Not applicable (this is a web app, not Electron) +- ❌ Much larger application size + +--- + +### **Option 5: Custom File Browser UI** + +**How it works:** +- Build custom file browser in React +- Backend API provides directory listings +- User navigates through folders in UI +- Select folder when found + +**Pros:** +- ✅ Full control over UI/UX +- ✅ Can show full paths +- ✅ No native dialogs needed + +**Cons:** +- ❌ Complex implementation +- ❌ Requires multiple API calls +- ❌ Slower user experience +- ❌ Need to handle permissions, hidden files, etc. + +--- + +## Recommendation + +**✅ Use Option 1: Backend API with Tkinter** + +This is the best solution because: +1. **tkinter is already used** in the project (face_processing.py) +2. **Simple implementation** - just one API endpoint +3. **Returns full paths** - solves the core problem +4. **Native dialogs** - familiar to users +5. **No additional dependencies** - tkinter is built into Python +6. **Cross-platform** - works on Windows, Linux, macOS + +### Implementation Steps + +1. **Create backend API endpoint** (`/api/v1/photos/browse-folder`) + - Use `tkinter.filedialog.askdirectory()` + - Return selected path as JSON + +2. **Add frontend API method** + - Call the new endpoint + - Handle response and populate path input + +3. **Update Browse button handler** + - Call backend API instead of browser picker + - Show loading state while waiting + - Handle errors gracefully + +4. **Fallback option** + - Keep browser-based picker as fallback + - Use if backend API fails or unavailable + +### Considerations + +- **Headless servers**: If backend runs on headless server, need X11 forwarding or use Option 3 (platform-specific tools) +- **Remote access**: If users access from remote machines, backend must be on same machine as user +- **Error handling**: Handle cases where tkinter dialog can't be shown (no display, permissions, etc.) + +--- + +## Quick Comparison Table + +| Solution | Full Path | Native Dialog | Dependencies | Complexity | Recommended | +|----------|-----------|---------------|--------------|------------|-------------| +| **Backend + Tkinter** | ✅ | ✅ | None (built-in) | Low | ✅ **YES** | +| Backend + PyQt | ✅ | ✅ | PyQt/PySide | Medium | ⚠️ Maybe | +| Platform Tools | ✅ | ✅ | zenity/kdialog/etc | High | ⚠️ Maybe | +| Custom UI | ✅ | ❌ | None | Very High | ❌ No | +| Electron | ✅ | ✅ | Electron | Very High | ❌ No | +| Browser API | ❌ | ✅ | None | Low | ❌ No | + +--- + +## Next Steps + +1. Implement backend API endpoint with tkinter +2. Add frontend API method +3. Update Browse button to use backend API +4. Add error handling and fallback +5. Test on all platforms (Windows, Linux, macOS) + diff --git a/MONOREPO_MIGRATION.md b/MONOREPO_MIGRATION.md new file mode 100644 index 0000000..5e83067 --- /dev/null +++ b/MONOREPO_MIGRATION.md @@ -0,0 +1,125 @@ +# Monorepo Migration Summary + +This document summarizes the migration from separate `punimtag` and `punimtag-viewer` projects to a unified monorepo structure. + +## Migration Date +December 2024 + +## Changes Made + +### Directory Structure + +**Before:** +``` +punimtag/ +├── src/web/ # Backend API +└── frontend/ # Admin React frontend + +punimtag-viewer/ # Separate repository +└── (Next.js viewer) +``` + +**After:** +``` +punimtag/ +├── backend/ # FastAPI backend (renamed from src/web) +├── admin-frontend/ # React admin interface (renamed from frontend) +└── viewer-frontend/ # Next.js viewer (moved from punimtag-viewer) +``` + +### Import Path Changes + +All Python imports have been updated: +- `from src.web.*` → `from backend.*` +- `import src.web.*` → `import backend.*` + +### Configuration Updates + +1. **install.sh**: Updated to install dependencies for both frontends +2. **package.json**: Created root package.json with workspace scripts +3. **run_api_with_worker.sh**: Updated to use `backend.app` instead of `src.web.app` +4. **run_worker.sh**: Updated to use `backend.worker` instead of `src.web.worker` +5. **docker-compose.yml**: Updated service commands to use `backend.*` paths + +### Environment Files + +- **admin-frontend/.env**: Backend API URL configuration +- **viewer-frontend/.env.local**: Database and NextAuth configuration + +### Port Configuration + +- **Admin Frontend**: Port 3000 (unchanged) +- **Viewer Frontend**: Port 3001 (configured in viewer-frontend/package.json) +- **Backend API**: Port 8000 (unchanged) + +## Running the Application + +### Development + +**Terminal 1 - Backend:** +```bash +source venv/bin/activate +export PYTHONPATH=$(pwd) +uvicorn backend.app:app --host 127.0.0.1 --port 8000 +``` + +**Terminal 2 - Admin Frontend:** +```bash +cd admin-frontend +npm run dev +``` + +**Terminal 3 - Viewer Frontend:** +```bash +cd viewer-frontend +npm run dev +``` + +### Using Root Scripts + +```bash +# Install all dependencies +npm run install:all + +# Run individual services +npm run dev:backend +npm run dev:admin +npm run dev:viewer +``` + +## Benefits + +1. **Unified Setup**: Single installation script for all components +2. **Easier Maintenance**: All code in one repository +3. **Shared Configuration**: Common environment variables and settings +4. **Simplified Deployment**: Single repository to deploy +5. **Better Organization**: Clear separation of admin and viewer interfaces + +## Migration Checklist + +- [x] Rename `src/web` to `backend` +- [x] Rename `frontend` to `admin-frontend` +- [x] Copy `punimtag-viewer` to `viewer-frontend` +- [x] Update all Python imports +- [x] Update all scripts +- [x] Update install.sh +- [x] Create root package.json +- [x] Update docker-compose.yml +- [x] Update README.md +- [x] Update scripts in scripts/ directory + +## Notes + +- The viewer frontend manages the `punimtag_auth` database +- Both frontends share the main `punimtag` database +- Backend API serves both frontends +- All database schemas remain unchanged + +## Next Steps + +1. Test all three services start correctly +2. Verify database connections work +3. Test authentication flows +4. Update CI/CD pipelines if applicable +5. Archive or remove the old `punimtag-viewer` repository + diff --git a/README.md b/README.md index 815ad4a..7b9cfb0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A fast, simple, and modern web application for organizing and tagging photos using state-of-the-art DeepFace AI with ArcFace recognition model. +**Monorepo Structure:** This project contains both the admin interface (React) and viewer interface (Next.js) in a unified repository for easier maintenance and setup. + --- ## 🎯 Features @@ -57,10 +59,16 @@ The script will: - ✅ Set up PostgreSQL databases (main + auth) - ✅ Create Python virtual environment - ✅ Install all Python dependencies -- ✅ Install all frontend dependencies +- ✅ Install all frontend dependencies (admin-frontend and viewer-frontend) - ✅ Create `.env` configuration files - ✅ Create necessary data directories +**Note:** After installation, you'll need to generate Prisma clients for the viewer-frontend: +```bash +cd viewer-frontend +npx prisma generate +``` + **Note:** On macOS or other systems, the script will skip system dependency installation. You'll need to install PostgreSQL and Redis manually. #### Option 2: Manual Installation @@ -78,17 +86,24 @@ source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt # Install frontend dependencies -cd frontend +cd admin-frontend npm install +cd ../viewer-frontend +npm install +# Generate Prisma clients for viewer-frontend (after setting up .env) +npx prisma generate cd .. ``` ### Database Setup -**PostgreSQL (Default - Network Database):** -The application is configured to use PostgreSQL by default. The application requires **two separate databases**: +**Database Configuration:** +The application uses **two separate databases**: 1. **Main database** (`punimtag`) - Stores photos, faces, people, tags, and backend user accounts + - **Default: SQLite** at `data/punimtag.db` (for development) + - **Optional: PostgreSQL** (for production) 2. **Auth database** (`punimtag_auth`) - Stores frontend website user accounts and moderation data + - **Required: PostgreSQL** (always uses PostgreSQL) Both database connections are configured via the `.env` file. @@ -140,12 +155,12 @@ Alternatively, use the automated script (requires sudo password): ``` **Configuration:** -The `.env` file contains both database connection strings: +The `.env` file in the project root contains database connection strings: ```bash -# Main application database -DATABASE_URL=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag +# Main application database (SQLite - default for development) +DATABASE_URL=sqlite:///data/punimtag.db -# Auth database (for frontend website users) +# Auth database (PostgreSQL - always required for frontend website users) DATABASE_URL_AUTH=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag_auth ``` @@ -153,23 +168,26 @@ DATABASE_URL_AUTH=postgresql+psycopg2://punimtag:punimtag_password@localhost:543 The database and all tables are automatically created on first startup. No manual migration is needed! The web application will: -- Connect to PostgreSQL using the `.env` configuration +- Connect to the database using the `.env` configuration - Create all required tables with the correct schema on startup - Match the desktop version schema exactly for compatibility -**Manual Setup (Optional):** -If you need to reset the database or create it manually: +**Note:** The main database uses SQLite by default for easier development. For production, you can switch to PostgreSQL by updating `DATABASE_URL` in `.env`. + +**SQLite (Default - Local Database):** +The main database uses SQLite by default for development. The `.env` file should contain: ```bash -source venv/bin/activate -export PYTHONPATH=/home/ladmin/Code/punimtag -# Recreate all tables from models -python scripts/recreate_tables_web.py +# Main database (SQLite - default for development) +DATABASE_URL=sqlite:///data/punimtag.db + +# Or use absolute path: +# DATABASE_URL=file:/home/ladmin/code/punimtag/data/punimtag.db ``` -**SQLite (Alternative - Local Database):** -To use SQLite instead of PostgreSQL, comment out or remove the `DATABASE_URL` line in `.env`, or set it to: +**PostgreSQL (Optional - for Production):** +To use PostgreSQL for the main database instead, set: ```bash -DATABASE_URL=sqlite:///data/punimtag.db +DATABASE_URL=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag ``` **Database Schema:** @@ -221,27 +239,46 @@ The separate auth database (`punimtag_auth`) stores frontend website user accoun redis-server ``` -#### Option 1: Manual Start (Recommended for Development) +#### Option 1: Using Helper Scripts (Recommended) -**Terminal 1 - Backend API:** +**Terminal 1 - Backend API + Worker:** ```bash -cd /home/ladmin/Code/punimtag -source venv/bin/activate -export PYTHONPATH=/home/ladmin/Code/punimtag -uvicorn src.web.app:app --host 127.0.0.1 --port 8000 +cd punimtag +./run_api_with_worker.sh ``` +This script will: +- Check if Redis is running (start it if needed) +- Ensure database schema is up to date +- Start the RQ worker in the background +- Start the FastAPI server +- Handle cleanup on Ctrl+C + You should see: ``` -✅ Database already initialized (7 tables exist) -✅ RQ worker started in background subprocess (PID: ...) -INFO: Started server process -INFO: Uvicorn running on http://127.0.0.1:8000 +✅ Database schema ready +🚀 Starting RQ worker... +🚀 Starting FastAPI server... +✅ Server running on http://127.0.0.1:8000 +✅ Worker running (PID: ...) +✅ API running (PID: ...) ``` -**Terminal 2 - Frontend:** +**Alternative: Start backend only (without worker):** ```bash -cd /home/ladmin/Code/punimtag/frontend +cd punimtag +./start_backend.sh +``` + +**Stop the backend:** +```bash +cd punimtag +./stop_backend.sh +``` + +**Terminal 2 - Admin Frontend:** +```bash +cd punimtag/admin-frontend npm run dev ``` @@ -251,44 +288,70 @@ VITE v5.4.21 ready in 811 ms ➜ Local: http://localhost:3000/ ``` -#### Option 2: Using Helper Script (Backend + Worker) - -**Terminal 1 - Backend API + Worker:** +**Terminal 3 - Viewer Frontend (Optional):** ```bash -cd /home/ladmin/Code/punimtag -./run_api_with_worker.sh -``` - -This script will: -- Check if Redis is running (start it if needed) -- Start the RQ worker in the background -- Start the FastAPI server -- Handle cleanup on Ctrl+C - -**Terminal 2 - Frontend:** -```bash -cd /home/ladmin/Code/punimtag/frontend +cd punimtag/viewer-frontend +# Generate Prisma clients (only needed once or after schema changes) +npx prisma generate npm run dev ``` -#### Access the Application +You should see: +``` +▲ Next.js 16.1.1 (Turbopack) +- Local: http://localhost:3001/ +``` -1. Open your browser to **http://localhost:3000** -2. Login with default credentials: - - Username: `admin` - - Password: `admin` -3. API documentation available at **http://127.0.0.1:8000/docs** +#### Option 2: Manual Start + +**Terminal 1 - Backend API:** +```bash +cd punimtag +source venv/bin/activate +export PYTHONPATH="$(pwd)" +python3 -m uvicorn backend.app:app --host 127.0.0.1 --port 8000 --reload +``` + +**Note:** If you encounter warnings about "Electron/Chromium" when running `uvicorn`, use `python3 -m uvicorn` instead, or use the helper scripts above. + +**Terminal 2 - Admin Frontend:** +```bash +cd punimtag/admin-frontend +npm run dev +``` + +**Terminal 3 - Viewer Frontend (Optional):** +```bash +cd punimtag/viewer-frontend +npx prisma generate # Only needed once or after schema changes +npm run dev +``` + +#### Access the Applications + +1. **Admin Interface**: Open your browser to **http://localhost:3000** + - Login with default credentials: + - Username: `admin` + - Password: `admin` +2. **Viewer Interface** (Optional): Open your browser to **http://localhost:3001** + - Public photo viewing interface + - Separate authentication system +3. **API Documentation**: Available at **http://127.0.0.1:8000/docs** #### Troubleshooting **Port 8000 already in use:** ```bash -# Find and kill the process using port 8000 +# Use the stop script +cd punimtag +./stop_backend.sh + +# Or manually find and kill the process lsof -i :8000 kill # Or use pkill -pkill -f "uvicorn.*app" +pkill -f "uvicorn.*backend.app" ``` **Port 3000 already in use:** @@ -297,7 +360,7 @@ pkill -f "uvicorn.*app" lsof -i :3000 kill -# Or change the port in frontend/vite.config.ts +# Or change the port in admin-frontend/vite.config.ts ``` **Redis not running:** @@ -306,17 +369,37 @@ kill sudo systemctl start redis-server # Or redis-server + +# Verify Redis is running +redis-cli ping # Should respond with "PONG" ``` +**Worker module not found error:** +If you see `ModuleNotFoundError: No module named 'backend'`: +- Make sure you're using the helper scripts (`./run_api_with_worker.sh` or `./start_backend.sh`) +- These scripts set PYTHONPATH correctly +- If running manually, ensure `export PYTHONPATH="$(pwd)"` is set + +**Python/Cursor interception warnings:** +If you see warnings about "Electron/Chromium" when running `uvicorn`: +- Use `python3 -m uvicorn` instead of just `uvicorn` +- Or use the helper scripts which handle this automatically + **Database issues:** ```bash -# Recreate all tables (WARNING: This will delete all data!) -cd /home/ladmin/Code/punimtag -source venv/bin/activate -export PYTHONPATH=/home/ladmin/Code/punimtag -python scripts/recreate_tables_web.py +# The database is automatically created on first startup +# If you need to reset it, delete the database file: +rm data/punimtag.db + +# The schema will be recreated on next startup ``` +**Viewer frontend shows 0 photos:** +- Make sure the database has photos (import them via admin frontend) +- Verify `DATABASE_URL` in `viewer-frontend/.env` points to the correct database +- Ensure Prisma client is generated: `cd viewer-frontend && npx prisma generate` +- Check that photos are marked as `processed: true` in the database + #### Important Notes - The database and tables are **automatically created on first startup** - no manual setup needed! @@ -340,14 +423,16 @@ python scripts/recreate_tables_web.py ``` punimtag/ -├── src/ # Source code -│ ├── web/ # Web backend -│ │ ├── api/ # API routers -│ │ ├── db/ # Database models and session -│ │ ├── schemas/ # Pydantic models -│ │ └── services/ # Business logic services -│ └── core/ # Legacy desktop business logic -├── frontend/ # React frontend +├── backend/ # FastAPI backend +│ ├── api/ # API routers +│ ├── db/ # Database models and session +│ ├── schemas/ # Pydantic models +│ ├── services/ # Business logic services +│ ├── constants/ # Constants and configuration +│ ├── utils/ # Utility functions +│ ├── app.py # FastAPI application +│ └── worker.py # RQ worker for background jobs +├── admin-frontend/ # React admin interface │ ├── src/ │ │ ├── api/ # API client │ │ ├── components/ # React components @@ -355,11 +440,20 @@ punimtag/ │ │ ├── hooks/ # Custom hooks │ │ └── pages/ # Page components │ └── package.json +├── viewer-frontend/ # Next.js viewer interface +│ ├── app/ # Next.js app router +│ ├── components/ # React components +│ ├── lib/ # Utilities and database +│ ├── prisma/ # Prisma schemas +│ └── package.json +├── src/ # Legacy desktop code +│ └── core/ # Legacy desktop business logic ├── tests/ # Test suite ├── docs/ # Documentation ├── data/ # Application data (database, images) -├── alembic/ # Database migrations -└── deploy/ # Docker deployment configs +├── scripts/ # Utility scripts +├── deploy/ # Docker deployment configs +└── package.json # Root package.json for monorepo ``` --- @@ -392,7 +486,9 @@ punimtag/ - ✅ All tables created automatically on startup: `photos`, `faces`, `people`, `person_encodings`, `tags`, `phototaglinkage` - ✅ Schema matches desktop version exactly for full compatibility - ✅ Indices configured for performance -- ✅ SQLite database at `data/punimtag.db` (auto-created if missing) +- ✅ SQLite database at `data/punimtag.db` (auto-created if missing, default for development) +- ✅ PostgreSQL support for production deployments +- ✅ Separate auth database (PostgreSQL) for frontend user accounts ### Image Ingestion & Processing @@ -482,23 +578,23 @@ punimtag/ ### Database -**PostgreSQL (Default - Network Database):** -The application uses PostgreSQL by default, configured via the `.env` file: +**SQLite (Default - Local Database):** +The main database uses SQLite by default for development, configured via the `.env` file: ```bash -# Main application database -DATABASE_URL=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag +# Main application database (SQLite - default) +DATABASE_URL=sqlite:///data/punimtag.db -# Auth database (for frontend website users) +# Auth database (PostgreSQL - always required for frontend website users) DATABASE_URL_AUTH=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag_auth ``` -**SQLite (Alternative - Local Database):** -To use SQLite instead, comment out or remove the `DATABASE_URL` line in `.env`, or set: +**PostgreSQL (Optional - for Production):** +To use PostgreSQL for the main database instead: ```bash -DATABASE_URL=sqlite:///data/punimtag.db +DATABASE_URL=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag ``` -**Note:** When using SQLite, the auth database (`DATABASE_URL_AUTH`) should still be configured as PostgreSQL if you need frontend website user authentication features. The auth database is optional but required for full multi-user functionality. +**Note:** The auth database (`DATABASE_URL_AUTH`) always uses PostgreSQL and is required for frontend website user authentication features. ### Environment Variables @@ -506,10 +602,10 @@ Configuration is managed via the `.env` file in the project root. A `.env.exampl **Required Configuration:** ```bash -# Database (PostgreSQL by default) -DATABASE_URL=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag +# Database (SQLite by default for development) +DATABASE_URL=sqlite:///data/punimtag.db -# Auth Database (for frontend website user accounts - separate from main database) +# Auth Database (PostgreSQL - always required for frontend website user accounts) DATABASE_URL_AUTH=postgresql+psycopg2://punimtag:punimtag_password@localhost:5432/punimtag_auth # JWT Secrets (change in production!) @@ -523,13 +619,45 @@ ADMIN_PASSWORD=admin PHOTO_STORAGE_DIR=data/uploads ``` -**Frontend Configuration:** -Create a `.env` file in the `frontend/` directory: +**Admin Frontend Configuration:** +Create a `.env` file in the `admin-frontend/` directory: ```bash # Backend API URL (must be accessible from browsers) VITE_API_URL=http://127.0.0.1:8000 ``` +**Viewer Frontend Configuration:** +Create a `.env` file in the `viewer-frontend/` directory: +```bash +# Main database connection (SQLite - matches backend default) +# Use absolute path for SQLite +DATABASE_URL=file:/home/ladmin/code/punimtag/data/punimtag.db + +# Auth database connection (PostgreSQL - always required) +DATABASE_URL_AUTH=postgresql://punimtag:punimtag_password@localhost:5432/punimtag_auth + +# Write-capable database connection (optional, falls back to DATABASE_URL if not set) +DATABASE_URL_WRITE=file:/home/ladmin/code/punimtag/data/punimtag.db + +# NextAuth configuration +NEXTAUTH_URL=http://localhost:3001 +NEXTAUTH_SECRET=dev-secret-key-change-in-production +``` + +**Generate Prisma Clients:** +After setting up the `.env` file, generate the Prisma clients: +```bash +cd viewer-frontend +npx prisma generate +``` + +**Important:** The viewer frontend uses **SQLite** for the main database (matching the backend default). The Prisma schema is configured for SQLite. If you change the backend to PostgreSQL, you'll need to: +1. Update `viewer-frontend/prisma/schema.prisma` to use `provider = "postgresql"` +2. Update `DATABASE_URL` in `viewer-frontend/.env` to the PostgreSQL connection string +3. Run `npx prisma generate` again + +**Note:** The viewer frontend uses the same database as the backend by default. For production deployments, you may want to create separate read-only and write users for better security. + **Note:** The `.env` file is automatically loaded by the application using `python-dotenv`. Environment variables can also be set directly in your shell if preferred. --- diff --git a/frontend/.eslintrc.cjs b/admin-frontend/.eslintrc.cjs similarity index 99% rename from frontend/.eslintrc.cjs rename to admin-frontend/.eslintrc.cjs index 3550926..b6fd40d 100644 --- a/frontend/.eslintrc.cjs +++ b/admin-frontend/.eslintrc.cjs @@ -49,3 +49,7 @@ module.exports = { + + + + diff --git a/frontend/README.md b/admin-frontend/README.md similarity index 100% rename from frontend/README.md rename to admin-frontend/README.md diff --git a/frontend/index.html b/admin-frontend/index.html similarity index 100% rename from frontend/index.html rename to admin-frontend/index.html diff --git a/frontend/package-lock.json b/admin-frontend/package-lock.json similarity index 88% rename from frontend/package-lock.json rename to admin-frontend/package-lock.json index bc09d11..bf4703f 100644 --- a/frontend/package-lock.json +++ b/admin-frontend/package-lock.json @@ -28,7 +28,7 @@ "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.4.0" } }, "node_modules/@alloc/quick-lru": { @@ -36,6 +36,7 @@ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -48,6 +49,7 @@ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -62,6 +64,7 @@ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -71,6 +74,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -101,6 +105,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -110,6 +115,7 @@ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", @@ -126,6 +132,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", @@ -142,6 +149,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -151,6 +159,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -160,6 +169,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" @@ -173,6 +183,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", @@ -190,6 +201,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -199,6 +211,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -208,6 +221,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -217,6 +231,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -226,6 +241,7 @@ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" @@ -239,6 +255,7 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" }, @@ -254,6 +271,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -269,6 +287,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -284,6 +303,7 @@ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", @@ -298,6 +318,7 @@ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -316,6 +337,7 @@ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -332,6 +354,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -348,6 +371,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -364,6 +388,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -380,6 +405,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -396,6 +422,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -412,6 +439,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -428,6 +456,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -444,6 +473,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -460,6 +490,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -476,6 +507,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -492,6 +524,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -508,6 +541,7 @@ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -524,6 +558,7 @@ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -540,6 +575,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -556,6 +592,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -572,6 +609,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -588,6 +626,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -604,6 +643,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -620,6 +660,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -636,6 +677,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -652,6 +694,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -668,6 +711,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -684,6 +728,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -697,6 +742,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -715,6 +761,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -724,6 +771,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -747,6 +795,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -757,6 +806,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -769,6 +819,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -779,6 +830,7 @@ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -793,6 +845,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -803,6 +856,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -815,6 +869,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -828,57 +883,15 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" @@ -889,6 +902,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -899,6 +913,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -907,13 +922,15 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -924,6 +941,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -937,6 +955,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -946,6 +965,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -954,20 +974,11 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -976,309 +987,334 @@ "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@tanstack/query-core": { - "version": "5.90.5", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz", - "integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==", + "version": "5.90.15", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.15.tgz", + "integrity": "sha512-mInIZNUZftbERE+/Hbtswfse49uUQwch46p+27gP9DWJL927UjnaWEF2t3RMOqBcXbfMdcNkPe06VyUIAZTV1g==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.90.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz", - "integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==", + "version": "5.90.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.15.tgz", + "integrity": "sha512-uQvnDDcTOgJouNtAyrgRej+Azf0U5WDov3PXmHFUBc+t1INnAYhIlpZtCGNBLwCN41b43yO7dPNZu8xWkUFBwQ==", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.5" + "@tanstack/query-core": "5.90.15" }, "funding": { "type": "github", @@ -1293,6 +1329,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1306,6 +1343,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -1315,6 +1353,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -1325,6 +1364,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" } @@ -1333,28 +1373,32 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", - "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -1362,6 +1406,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, + "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } @@ -1370,13 +1415,15 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", @@ -1412,6 +1459,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -1440,6 +1488,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" @@ -1457,6 +1506,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", @@ -1484,6 +1534,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, + "license": "MIT", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -1497,6 +1548,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", @@ -1525,6 +1577,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", @@ -1550,6 +1603,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" @@ -1566,13 +1620,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", @@ -1593,6 +1649,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1605,6 +1662,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1614,6 +1672,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1630,6 +1689,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1639,6 +1699,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1653,13 +1714,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1672,13 +1735,15 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", @@ -1725,6 +1790,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1840,12 +1906,13 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", "dev": true, "funding": [ { @@ -1861,11 +1928,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -1896,9 +1963,10 @@ } }, "node_modules/axios": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", - "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -1909,13 +1977,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.22", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.22.tgz", - "integrity": "sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==", + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "dev": true, + "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -1925,6 +1995,7 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -1937,6 +2008,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1946,6 +2018,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1954,9 +2027,9 @@ } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1972,12 +2045,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2009,6 +2083,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2039,6 +2114,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2048,14 +2124,15 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001752", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz", - "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==", + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", "dev": true, "funding": [ { @@ -2070,13 +2147,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2093,6 +2172,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2117,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2129,6 +2210,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2140,12 +2222,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2158,6 +2242,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -2166,19 +2251,22 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2193,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -2201,10 +2290,11 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -2265,6 +2355,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2281,7 +2372,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/define-data-property": { "version": "1.1.4", @@ -2323,6 +2415,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -2331,13 +2424,15 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2349,13 +2444,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2367,6 +2464,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2376,28 +2474,17 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, "node_modules/electron-to-chromium": { - "version": "1.5.244", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", - "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -2467,6 +2554,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2475,32 +2563,33 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -2511,6 +2600,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2522,6 +2612,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2569,6 +2660,7 @@ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2606,6 +2698,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2615,6 +2708,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2628,6 +2722,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2716,6 +2811,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2724,10 +2820,11 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", - "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=8.40" } @@ -2769,24 +2866,6 @@ "node": "*" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2802,6 +2881,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2818,6 +2898,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2830,6 +2911,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2840,6 +2922,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2852,6 +2935,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -2869,6 +2953,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -2881,6 +2966,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2893,6 +2979,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -2902,6 +2989,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -2910,13 +2998,15 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2933,6 +3023,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2944,19 +3035,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -2966,6 +3060,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -2978,6 +3073,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2990,6 +3086,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3006,6 +3103,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3019,7 +3117,8 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -3031,6 +3130,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -3056,26 +3156,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3088,15 +3173,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -3104,7 +3190,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -3112,6 +3199,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3124,6 +3212,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3174,6 +3263,7 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -3182,6 +3272,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3205,6 +3296,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3237,6 +3329,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3257,6 +3350,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3269,6 +3363,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3279,6 +3374,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3291,6 +3387,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -3323,6 +3420,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3342,6 +3440,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3353,7 +3452,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", @@ -3373,6 +3473,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3410,6 +3511,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3421,6 +3523,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3435,6 +3538,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3447,6 +3551,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -3456,6 +3561,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3472,6 +3578,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -3482,6 +3589,7 @@ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3491,7 +3599,8 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -3567,6 +3676,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3609,6 +3719,7 @@ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -3659,6 +3770,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3679,15 +3791,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -3713,6 +3816,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3751,6 +3855,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3777,6 +3882,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3937,7 +4043,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -3957,26 +4064,12 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -3984,13 +4077,15 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4003,6 +4098,7 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4014,25 +4110,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -4061,6 +4161,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -4070,6 +4171,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4083,6 +4185,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -4094,13 +4197,15 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4115,12 +4220,14 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4133,6 +4240,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -4141,6 +4249,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4150,6 +4259,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -4159,6 +4269,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4171,6 +4282,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4179,6 +4291,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -4191,6 +4304,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4201,26 +4315,19 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -4238,6 +4345,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4249,28 +4357,22 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4280,6 +4382,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4289,6 +4392,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4396,6 +4500,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -4405,6 +4510,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4440,6 +4546,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4455,6 +4562,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4465,17 +4573,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4488,6 +4591,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4497,6 +4601,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4506,6 +4611,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4514,35 +4620,15 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4551,13 +4637,15 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4570,6 +4658,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4579,6 +4668,7 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4612,6 +4702,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4626,6 +4717,7 @@ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -4638,6 +4730,27 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/postcss-js": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", @@ -4653,6 +4766,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -4678,6 +4792,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.1.1" }, @@ -4720,6 +4835,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.1" }, @@ -4735,6 +4851,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4747,13 +4864,15 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -4773,13 +4892,15 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4802,12 +4923,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -4819,6 +4942,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4839,16 +4963,18 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.1" }, "engines": { "node": ">=14.0.0" @@ -4858,12 +4984,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" }, "engines": { "node": ">=14.0.0" @@ -4878,6 +5005,7 @@ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -4887,6 +5015,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -4939,21 +5068,19 @@ } }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4963,6 +5090,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4972,6 +5100,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4983,6 +5112,7 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -4994,10 +5124,11 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -5009,28 +5140,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" } }, @@ -5053,6 +5184,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -5116,6 +5248,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -5125,6 +5258,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5186,6 +5320,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5198,6 +5333,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5278,23 +5414,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5304,6 +5429,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5322,71 +5448,6 @@ "node": ">= 0.4" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -5490,19 +5551,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5515,6 +5564,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5523,17 +5573,18 @@ } }, "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -5544,46 +5595,12 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5596,6 +5613,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5604,10 +5622,11 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -5640,17 +5659,40 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -5660,6 +5702,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -5667,11 +5710,60 @@ "node": ">=0.8" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -5684,6 +5776,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -5695,13 +5788,15 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5714,6 +5809,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -5804,6 +5900,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5832,9 +5929,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -5850,6 +5947,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -5866,6 +5964,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -5874,13 +5973,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -5940,6 +6041,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6044,121 +6146,31 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/frontend/package.json b/admin-frontend/package.json similarity index 97% rename from frontend/package.json rename to admin-frontend/package.json index b22c5f5..dfc5e11 100644 --- a/frontend/package.json +++ b/admin-frontend/package.json @@ -30,6 +30,6 @@ "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.4.0" } } diff --git a/frontend/postcss.config.js b/admin-frontend/postcss.config.js similarity index 100% rename from frontend/postcss.config.js rename to admin-frontend/postcss.config.js diff --git a/frontend/src/App.tsx b/admin-frontend/src/App.tsx similarity index 100% rename from frontend/src/App.tsx rename to admin-frontend/src/App.tsx diff --git a/frontend/src/api/auth.ts b/admin-frontend/src/api/auth.ts similarity index 100% rename from frontend/src/api/auth.ts rename to admin-frontend/src/api/auth.ts diff --git a/frontend/src/api/authUsers.ts b/admin-frontend/src/api/authUsers.ts similarity index 100% rename from frontend/src/api/authUsers.ts rename to admin-frontend/src/api/authUsers.ts diff --git a/frontend/src/api/client.ts b/admin-frontend/src/api/client.ts similarity index 58% rename from frontend/src/api/client.ts rename to admin-frontend/src/api/client.ts index 162bdac..a5ad15e 100644 --- a/frontend/src/api/client.ts +++ b/admin-frontend/src/api/client.ts @@ -1,5 +1,8 @@ import axios from 'axios' +// Get API base URL from environment variable or use default +// The .env file should contain: VITE_API_URL=http://127.0.0.1:8000 +// Alternatively, Vite proxy can be used (configured in vite.config.ts) by setting VITE_API_URL to empty string const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8000' export const apiClient = axios.create({ @@ -18,10 +21,27 @@ apiClient.interceptors.request.use((config) => { return config }) -// Handle 401 errors +// Handle 401 errors and network errors apiClient.interceptors.response.use( (response) => response, (error) => { + // Handle network errors (no response from server) + if (!error.response && (error.message === 'Network Error' || error.code === 'ERR_NETWORK')) { + // Check if user is logged in + const token = localStorage.getItem('access_token') + if (!token) { + // Not logged in - redirect to login + const isLoginPage = window.location.pathname === '/login' + if (!isLoginPage) { + window.location.href = '/login' + return Promise.reject(error) + } + } + // If logged in but network error, it's a connection issue + console.error('Network Error:', error) + } + + // Handle 401 Unauthorized if (error.response?.status === 401) { // Don't redirect if we're already on the login page (prevents clearing error messages) const isLoginPage = window.location.pathname === '/login' diff --git a/frontend/src/api/faces.ts b/admin-frontend/src/api/faces.ts similarity index 100% rename from frontend/src/api/faces.ts rename to admin-frontend/src/api/faces.ts diff --git a/frontend/src/api/jobs.ts b/admin-frontend/src/api/jobs.ts similarity index 91% rename from frontend/src/api/jobs.ts rename to admin-frontend/src/api/jobs.ts index e3caef3..86366bd 100644 --- a/frontend/src/api/jobs.ts +++ b/admin-frontend/src/api/jobs.ts @@ -26,6 +26,7 @@ export const jobsApi = { }, streamJobProgress: (jobId: string): EventSource => { + // EventSource needs absolute URL - use VITE_API_URL or fallback to direct backend URL const baseURL = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8000' return new EventSource(`${baseURL}/api/v1/jobs/stream/${jobId}`) }, diff --git a/frontend/src/api/pendingIdentifications.ts b/admin-frontend/src/api/pendingIdentifications.ts similarity index 100% rename from frontend/src/api/pendingIdentifications.ts rename to admin-frontend/src/api/pendingIdentifications.ts diff --git a/frontend/src/api/pendingLinkages.ts b/admin-frontend/src/api/pendingLinkages.ts similarity index 100% rename from frontend/src/api/pendingLinkages.ts rename to admin-frontend/src/api/pendingLinkages.ts diff --git a/frontend/src/api/pendingPhotos.ts b/admin-frontend/src/api/pendingPhotos.ts similarity index 100% rename from frontend/src/api/pendingPhotos.ts rename to admin-frontend/src/api/pendingPhotos.ts diff --git a/frontend/src/api/people.ts b/admin-frontend/src/api/people.ts similarity index 100% rename from frontend/src/api/people.ts rename to admin-frontend/src/api/people.ts diff --git a/frontend/src/api/photos.ts b/admin-frontend/src/api/photos.ts similarity index 97% rename from frontend/src/api/photos.ts rename to admin-frontend/src/api/photos.ts index f8f7e5d..7e5a15d 100644 --- a/frontend/src/api/photos.ts +++ b/admin-frontend/src/api/photos.ts @@ -74,6 +74,7 @@ export const photosApi = { }, streamJobProgress: (jobId: string): EventSource => { + // EventSource needs absolute URL - use VITE_API_URL or fallback to direct backend URL const baseURL = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8000' return new EventSource(`${baseURL}/api/v1/jobs/stream/${jobId}`) }, diff --git a/frontend/src/api/reportedPhotos.ts b/admin-frontend/src/api/reportedPhotos.ts similarity index 100% rename from frontend/src/api/reportedPhotos.ts rename to admin-frontend/src/api/reportedPhotos.ts diff --git a/frontend/src/api/rolePermissions.ts b/admin-frontend/src/api/rolePermissions.ts similarity index 99% rename from frontend/src/api/rolePermissions.ts rename to admin-frontend/src/api/rolePermissions.ts index 13358c6..b135a43 100644 --- a/frontend/src/api/rolePermissions.ts +++ b/admin-frontend/src/api/rolePermissions.ts @@ -36,3 +36,7 @@ export const rolePermissionsApi = { + + + + diff --git a/frontend/src/api/tags.ts b/admin-frontend/src/api/tags.ts similarity index 100% rename from frontend/src/api/tags.ts rename to admin-frontend/src/api/tags.ts diff --git a/frontend/src/api/users.ts b/admin-frontend/src/api/users.ts similarity index 100% rename from frontend/src/api/users.ts rename to admin-frontend/src/api/users.ts diff --git a/frontend/src/api/videos.ts b/admin-frontend/src/api/videos.ts similarity index 99% rename from frontend/src/api/videos.ts rename to admin-frontend/src/api/videos.ts index 7d73f8d..b0e77a7 100644 --- a/frontend/src/api/videos.ts +++ b/admin-frontend/src/api/videos.ts @@ -122,3 +122,7 @@ export default videosApi + + + + diff --git a/frontend/src/components/AdminRoute.tsx b/admin-frontend/src/components/AdminRoute.tsx similarity index 100% rename from frontend/src/components/AdminRoute.tsx rename to admin-frontend/src/components/AdminRoute.tsx diff --git a/frontend/src/components/Layout.tsx b/admin-frontend/src/components/Layout.tsx similarity index 100% rename from frontend/src/components/Layout.tsx rename to admin-frontend/src/components/Layout.tsx diff --git a/frontend/src/components/PasswordChangeModal.tsx b/admin-frontend/src/components/PasswordChangeModal.tsx similarity index 100% rename from frontend/src/components/PasswordChangeModal.tsx rename to admin-frontend/src/components/PasswordChangeModal.tsx diff --git a/frontend/src/components/PhotoViewer.tsx b/admin-frontend/src/components/PhotoViewer.tsx similarity index 100% rename from frontend/src/components/PhotoViewer.tsx rename to admin-frontend/src/components/PhotoViewer.tsx diff --git a/frontend/src/context/AuthContext.tsx b/admin-frontend/src/context/AuthContext.tsx similarity index 100% rename from frontend/src/context/AuthContext.tsx rename to admin-frontend/src/context/AuthContext.tsx diff --git a/frontend/src/context/DeveloperModeContext.tsx b/admin-frontend/src/context/DeveloperModeContext.tsx similarity index 100% rename from frontend/src/context/DeveloperModeContext.tsx rename to admin-frontend/src/context/DeveloperModeContext.tsx diff --git a/frontend/src/hooks/useAuth.ts b/admin-frontend/src/hooks/useAuth.ts similarity index 100% rename from frontend/src/hooks/useAuth.ts rename to admin-frontend/src/hooks/useAuth.ts diff --git a/frontend/src/hooks/useInactivityTimeout.ts b/admin-frontend/src/hooks/useInactivityTimeout.ts similarity index 100% rename from frontend/src/hooks/useInactivityTimeout.ts rename to admin-frontend/src/hooks/useInactivityTimeout.ts diff --git a/frontend/src/index.css b/admin-frontend/src/index.css similarity index 100% rename from frontend/src/index.css rename to admin-frontend/src/index.css diff --git a/frontend/src/main.tsx b/admin-frontend/src/main.tsx similarity index 100% rename from frontend/src/main.tsx rename to admin-frontend/src/main.tsx diff --git a/frontend/src/pages/ApproveIdentified.tsx b/admin-frontend/src/pages/ApproveIdentified.tsx similarity index 97% rename from frontend/src/pages/ApproveIdentified.tsx rename to admin-frontend/src/pages/ApproveIdentified.tsx index e159051..5438757 100644 --- a/frontend/src/pages/ApproveIdentified.tsx +++ b/admin-frontend/src/pages/ApproveIdentified.tsx @@ -30,7 +30,17 @@ export default function ApproveIdentified() { const response = await pendingIdentificationsApi.list(includeDenied) setPendingIdentifications(response.items) } catch (err: any) { - setError(err.response?.data?.detail || err.message || 'Failed to load pending identifications') + let errorMessage = 'Failed to load pending identifications' + if (err.response?.data?.detail) { + errorMessage = err.response.data.detail + } else if (err.message) { + errorMessage = err.message + // Provide more context for network errors + if (err.message === 'Network Error' || err.code === 'ERR_NETWORK') { + errorMessage = `Network Error: Cannot connect to backend API (${apiClient.defaults.baseURL}). Please check:\n1. Backend is running\n2. You are logged in\n3. CORS is configured correctly` + } + } + setError(errorMessage) console.error('Error loading pending identifications:', err) } finally { setLoading(false) diff --git a/frontend/src/pages/AutoMatch.tsx b/admin-frontend/src/pages/AutoMatch.tsx similarity index 100% rename from frontend/src/pages/AutoMatch.tsx rename to admin-frontend/src/pages/AutoMatch.tsx diff --git a/frontend/src/pages/Dashboard.tsx b/admin-frontend/src/pages/Dashboard.tsx similarity index 100% rename from frontend/src/pages/Dashboard.tsx rename to admin-frontend/src/pages/Dashboard.tsx diff --git a/frontend/src/pages/FacesMaintenance.tsx b/admin-frontend/src/pages/FacesMaintenance.tsx similarity index 100% rename from frontend/src/pages/FacesMaintenance.tsx rename to admin-frontend/src/pages/FacesMaintenance.tsx diff --git a/frontend/src/pages/Help.tsx b/admin-frontend/src/pages/Help.tsx similarity index 100% rename from frontend/src/pages/Help.tsx rename to admin-frontend/src/pages/Help.tsx diff --git a/frontend/src/pages/Identify.tsx b/admin-frontend/src/pages/Identify.tsx similarity index 100% rename from frontend/src/pages/Identify.tsx rename to admin-frontend/src/pages/Identify.tsx diff --git a/frontend/src/pages/Login.tsx b/admin-frontend/src/pages/Login.tsx similarity index 100% rename from frontend/src/pages/Login.tsx rename to admin-frontend/src/pages/Login.tsx diff --git a/frontend/src/pages/ManagePhotos.tsx b/admin-frontend/src/pages/ManagePhotos.tsx similarity index 100% rename from frontend/src/pages/ManagePhotos.tsx rename to admin-frontend/src/pages/ManagePhotos.tsx diff --git a/frontend/src/pages/ManageUsers.tsx b/admin-frontend/src/pages/ManageUsers.tsx similarity index 100% rename from frontend/src/pages/ManageUsers.tsx rename to admin-frontend/src/pages/ManageUsers.tsx diff --git a/frontend/src/pages/Modify.tsx b/admin-frontend/src/pages/Modify.tsx similarity index 100% rename from frontend/src/pages/Modify.tsx rename to admin-frontend/src/pages/Modify.tsx diff --git a/frontend/src/pages/PendingPhotos.tsx b/admin-frontend/src/pages/PendingPhotos.tsx similarity index 100% rename from frontend/src/pages/PendingPhotos.tsx rename to admin-frontend/src/pages/PendingPhotos.tsx diff --git a/frontend/src/pages/Process.tsx b/admin-frontend/src/pages/Process.tsx similarity index 100% rename from frontend/src/pages/Process.tsx rename to admin-frontend/src/pages/Process.tsx diff --git a/frontend/src/pages/ReportedPhotos.tsx b/admin-frontend/src/pages/ReportedPhotos.tsx similarity index 100% rename from frontend/src/pages/ReportedPhotos.tsx rename to admin-frontend/src/pages/ReportedPhotos.tsx diff --git a/frontend/src/pages/Scan.tsx b/admin-frontend/src/pages/Scan.tsx similarity index 100% rename from frontend/src/pages/Scan.tsx rename to admin-frontend/src/pages/Scan.tsx diff --git a/frontend/src/pages/Search.tsx b/admin-frontend/src/pages/Search.tsx similarity index 99% rename from frontend/src/pages/Search.tsx rename to admin-frontend/src/pages/Search.tsx index 1b3a4de..e794181 100644 --- a/frontend/src/pages/Search.tsx +++ b/admin-frontend/src/pages/Search.tsx @@ -306,9 +306,10 @@ export default function Search() { // Auto-run search for no_faces and no_tags // No alert shown when no photos found - } catch (error) { + } catch (error: any) { console.error('Error searching photos:', error) - alert('Error searching photos. Please try again.') + const errorMessage = error.response?.data?.detail || error.message || 'Unknown error' + alert(`Error searching photos: ${errorMessage}\n\nPlease check:\n1. Backend API is running (http://127.0.0.1:8000)\n2. You are logged in\n3. Database connection is working`) } finally { setLoading(false) } diff --git a/frontend/src/pages/Settings.tsx b/admin-frontend/src/pages/Settings.tsx similarity index 100% rename from frontend/src/pages/Settings.tsx rename to admin-frontend/src/pages/Settings.tsx diff --git a/frontend/src/pages/Tags.tsx b/admin-frontend/src/pages/Tags.tsx similarity index 100% rename from frontend/src/pages/Tags.tsx rename to admin-frontend/src/pages/Tags.tsx diff --git a/frontend/src/pages/UserTaggedPhotos.tsx b/admin-frontend/src/pages/UserTaggedPhotos.tsx similarity index 100% rename from frontend/src/pages/UserTaggedPhotos.tsx rename to admin-frontend/src/pages/UserTaggedPhotos.tsx diff --git a/frontend/src/vite-env.d.ts b/admin-frontend/src/vite-env.d.ts similarity index 100% rename from frontend/src/vite-env.d.ts rename to admin-frontend/src/vite-env.d.ts diff --git a/frontend/tailwind.config.js b/admin-frontend/tailwind.config.js similarity index 100% rename from frontend/tailwind.config.js rename to admin-frontend/tailwind.config.js diff --git a/frontend/tsconfig.json b/admin-frontend/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to admin-frontend/tsconfig.json diff --git a/frontend/tsconfig.node.json b/admin-frontend/tsconfig.node.json similarity index 100% rename from frontend/tsconfig.node.json rename to admin-frontend/tsconfig.node.json diff --git a/frontend/vite.config.ts b/admin-frontend/vite.config.ts similarity index 100% rename from frontend/vite.config.ts rename to admin-frontend/vite.config.ts diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/web/api/__init__.py b/backend/api/__init__.py similarity index 100% rename from src/web/api/__init__.py rename to backend/api/__init__.py diff --git a/src/web/api/auth.py b/backend/api/auth.py similarity index 95% rename from src/web/api/auth.py rename to backend/api/auth.py index 93c6d6e..8f023f6 100644 --- a/src/web/api/auth.py +++ b/backend/api/auth.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from datetime import datetime, timedelta from typing import Annotated @@ -10,15 +11,15 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import JWTError, jwt from sqlalchemy.orm import Session -from src.web.constants.roles import ( +from backend.constants.roles import ( DEFAULT_ADMIN_ROLE, DEFAULT_USER_ROLE, ROLE_VALUES, ) -from src.web.db.session import get_db -from src.web.db.models import User -from src.web.utils.password import verify_password, hash_password -from src.web.schemas.auth import ( +from backend.db.session import get_db +from backend.db.models import User +from backend.utils.password import verify_password, hash_password +from backend.schemas.auth import ( LoginRequest, RefreshRequest, TokenResponse, @@ -26,7 +27,7 @@ from src.web.schemas.auth import ( PasswordChangeRequest, PasswordChangeResponse, ) -from src.web.services.role_permissions import fetch_role_permissions_map +from backend.services.role_permissions import fetch_role_permissions_map router = APIRouter(prefix="/auth", tags=["auth"]) security = HTTPBearer() @@ -37,9 +38,9 @@ ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 360 REFRESH_TOKEN_EXPIRE_DAYS = 7 -# Single user mode placeholder -SINGLE_USER_USERNAME = "admin" -SINGLE_USER_PASSWORD = "admin" # Change in production +# Single user mode placeholder - read from environment or use defaults +SINGLE_USER_USERNAME = os.getenv("ADMIN_USERNAME", "admin") +SINGLE_USER_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin") # Change in production def create_access_token(data: dict, expires_delta: timedelta) -> str: @@ -96,7 +97,7 @@ def get_current_user_with_id( # If user doesn't exist, create them (for bootstrap scenarios) if not user: - from src.web.utils.password import hash_password + from backend.utils.password import hash_password # Generate unique email to avoid conflicts base_email = f"{username}@example.com" @@ -255,7 +256,7 @@ def get_current_user_info( # If no admins exist, bootstrap current user as admin if admin_count == 0: - from src.web.utils.password import hash_password + from backend.utils.password import hash_password # Generate unique email to avoid conflicts base_email = f"{username}@example.com" diff --git a/src/web/api/auth_users.py b/backend/api/auth_users.py similarity index 99% rename from src/web/api/auth_users.py rename to backend/api/auth_users.py index ca4cc53..af9016b 100644 --- a/src/web/api/auth_users.py +++ b/backend/api/auth_users.py @@ -10,16 +10,16 @@ from fastapi.responses import JSONResponse from sqlalchemy import text from sqlalchemy.orm import Session -from src.web.api.auth import get_current_user -from src.web.api.users import get_current_admin_user -from src.web.db.session import get_auth_db, get_db -from src.web.schemas.auth_users import ( +from backend.api.auth import get_current_user +from backend.api.users import get_current_admin_user +from backend.db.session import get_auth_db, get_db +from backend.schemas.auth_users import ( AuthUserCreateRequest, AuthUserResponse, AuthUserUpdateRequest, AuthUsersListResponse, ) -from src.web.utils.password import hash_password +from backend.utils.password import hash_password router = APIRouter(prefix="/auth-users", tags=["auth-users"]) logger = logging.getLogger(__name__) diff --git a/src/web/api/faces.py b/backend/api/faces.py similarity index 98% rename from src/web/api/faces.py rename to backend/api/faces.py index 784913b..d9dfff3 100644 --- a/src/web/api/faces.py +++ b/backend/api/faces.py @@ -10,9 +10,9 @@ from sqlalchemy import func from sqlalchemy.orm import Session from typing import Annotated -from src.web.db.session import get_db -from src.web.api.auth import get_current_user_with_id -from src.web.schemas.faces import ( +from backend.db.session import get_db +from backend.api.auth import get_current_user_with_id +from backend.schemas.faces import ( ProcessFacesRequest, ProcessFacesResponse, UnidentifiedFacesQuery, @@ -41,9 +41,9 @@ from src.web.schemas.faces import ( DeleteFacesRequest, DeleteFacesResponse, ) -from src.web.schemas.people import PersonCreateRequest, PersonResponse -from src.web.db.models import Face, Person, PersonEncoding, Photo -from src.web.services.face_service import ( +from backend.schemas.people import PersonCreateRequest, PersonResponse +from backend.db.models import Face, Person, PersonEncoding, Photo +from backend.services.face_service import ( list_unidentified_faces, find_similar_faces, calculate_batch_similarities, @@ -81,7 +81,7 @@ def process_faces(request: ProcessFacesRequest) -> ProcessFacesResponse: # Enqueue face processing job # Pass function as string path to avoid serialization issues job = queue.enqueue( - "src.web.services.tasks.process_faces_task", + "backend.services.tasks.process_faces_task", batch_size=request.batch_size, detector_backend=request.detector_backend, model_name=request.model_name, @@ -350,7 +350,7 @@ def get_face_crop(face_id: int, db: Session = Depends(get_db)) -> Response: import ast import tempfile from PIL import Image - from src.web.db.models import Face, Photo + from backend.db.models import Face, Photo from src.utils.exif_utils import EXIFOrientationHandler face = db.query(Face).filter(Face.id == face_id).first() @@ -593,7 +593,7 @@ def auto_match_faces( - Only auto-accepts matches with similarity >= threshold - Only auto-accepts faces with quality > 50% (quality_score > 0.5) """ - from src.web.db.models import Person, Photo + from backend.db.models import Person, Photo from sqlalchemy import func # Track statistics for auto-accept @@ -755,7 +755,7 @@ def get_auto_match_people( Note: Only returns people if there are unidentified faces in the database (since people can't have matches if there are no unidentified faces). """ - from src.web.db.models import Person, Photo + from backend.db.models import Person, Photo # Get people list (fast - no match calculations, but checks for unidentified faces) people_data = get_auto_match_people_list( @@ -814,7 +814,7 @@ def get_auto_match_person_matches( This endpoint is called on-demand when user navigates to a person. """ - from src.web.db.models import Photo + from backend.db.models import Photo # Get matches for this person similar_faces = get_person_matches_service( diff --git a/src/web/api/health.py b/backend/api/health.py similarity index 100% rename from src/web/api/health.py rename to backend/api/health.py diff --git a/src/web/api/jobs.py b/backend/api/jobs.py similarity index 99% rename from src/web/api/jobs.py rename to backend/api/jobs.py index b100f19..c4a9512 100644 --- a/src/web/api/jobs.py +++ b/backend/api/jobs.py @@ -12,7 +12,7 @@ from redis import Redis import json import time -from src.web.schemas.jobs import JobResponse, JobStatus +from backend.schemas.jobs import JobResponse, JobStatus router = APIRouter(prefix="/jobs", tags=["jobs"]) diff --git a/src/web/api/metrics.py b/backend/api/metrics.py similarity index 100% rename from src/web/api/metrics.py rename to backend/api/metrics.py diff --git a/src/web/api/pending_identifications.py b/backend/api/pending_identifications.py similarity index 98% rename from src/web/api/pending_identifications.py rename to backend/api/pending_identifications.py index 810aa10..110ab59 100644 --- a/src/web/api/pending_identifications.py +++ b/backend/api/pending_identifications.py @@ -10,11 +10,11 @@ from pydantic import BaseModel, ConfigDict from sqlalchemy import text, func from sqlalchemy.orm import Session -from src.web.constants.roles import DEFAULT_USER_ROLE -from src.web.db.session import get_auth_db, get_db -from src.web.db.models import Face, Person, PersonEncoding, User -from src.web.api.users import get_current_admin_user, require_feature_permission -from src.web.utils.password import hash_password +from backend.constants.roles import DEFAULT_USER_ROLE +from backend.db.session import get_auth_db, get_db +from backend.db.models import Face, Person, PersonEncoding, User +from backend.api.users import get_current_admin_user, require_feature_permission +from backend.utils.password import hash_password router = APIRouter(prefix="/pending-identifications", tags=["pending-identifications"]) diff --git a/src/web/api/pending_linkages.py b/backend/api/pending_linkages.py similarity index 98% rename from src/web/api/pending_linkages.py rename to backend/api/pending_linkages.py index 73e9d07..00e1b0d 100644 --- a/src/web/api/pending_linkages.py +++ b/backend/api/pending_linkages.py @@ -10,9 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import text from sqlalchemy.orm import Session -from src.web.api.users import require_feature_permission -from src.web.db.models import Photo, PhotoTagLinkage, Tag -from src.web.db.session import get_auth_db, get_db +from backend.api.users import require_feature_permission +from backend.db.models import Photo, PhotoTagLinkage, Tag +from backend.db.session import get_auth_db, get_db router = APIRouter(prefix="/pending-linkages", tags=["pending-linkages"]) diff --git a/src/web/api/pending_photos.py b/backend/api/pending_photos.py similarity index 98% rename from src/web/api/pending_photos.py rename to backend/api/pending_photos.py index aad73f7..3c6d6f6 100644 --- a/src/web/api/pending_photos.py +++ b/backend/api/pending_photos.py @@ -13,11 +13,11 @@ from pydantic import BaseModel, ConfigDict from sqlalchemy import text from sqlalchemy.orm import Session -from src.web.db.session import get_auth_db, get_db -from src.web.api.users import get_current_admin_user, require_feature_permission -from src.web.api.auth import get_current_user -from src.web.services.photo_service import import_photo_from_path, calculate_file_hash -from src.web.settings import PHOTO_STORAGE_DIR +from backend.db.session import get_auth_db, get_db +from backend.api.users import get_current_admin_user, require_feature_permission +from backend.api.auth import get_current_user +from backend.services.photo_service import import_photo_from_path, calculate_file_hash +from backend.settings import PHOTO_STORAGE_DIR router = APIRouter(prefix="/pending-photos", tags=["pending-photos"]) diff --git a/src/web/api/people.py b/backend/api/people.py similarity index 96% rename from src/web/api/people.py rename to backend/api/people.py index 5de1bb0..27aadc4 100644 --- a/src/web/api/people.py +++ b/backend/api/people.py @@ -8,10 +8,10 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from sqlalchemy import func from sqlalchemy.orm import Session -from src.web.db.session import get_db -from src.web.db.models import Person, Face, PersonEncoding, PhotoPersonLinkage, Photo -from src.web.api.auth import get_current_user_with_id -from src.web.schemas.people import ( +from backend.db.session import get_db +from backend.db.models import Person, Face, PersonEncoding, PhotoPersonLinkage, Photo +from backend.api.auth import get_current_user_with_id +from backend.schemas.people import ( PeopleListResponse, PersonCreateRequest, PersonResponse, @@ -19,8 +19,8 @@ from src.web.schemas.people import ( PersonWithFacesResponse, PeopleWithFacesListResponse, ) -from src.web.schemas.faces import PersonFacesResponse, PersonFaceItem, AcceptMatchesRequest, IdentifyFaceResponse -from src.web.services.face_service import accept_auto_match_matches +from backend.schemas.faces import PersonFacesResponse, PersonFaceItem, AcceptMatchesRequest, IdentifyFaceResponse +from backend.services.face_service import accept_auto_match_matches router = APIRouter(prefix="/people", tags=["people"]) @@ -179,7 +179,7 @@ def get_person_faces(person_id: int, db: Session = Depends(get_db)) -> PersonFac if not person: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Person {person_id} not found") - from src.web.db.models import Photo + from backend.db.models import Photo faces = ( db.query(Face) @@ -260,7 +260,7 @@ def accept_matches( 3. Updates person encodings (removes old, adds current) Tracks which user identified the faces. """ - from src.web.api.auth import get_current_user_with_id + from backend.api.auth import get_current_user_with_id user_id = current_user["user_id"] identified_count, updated_count = accept_auto_match_matches( diff --git a/src/web/api/photos.py b/backend/api/photos.py similarity index 97% rename from src/web/api/photos.py rename to backend/api/photos.py index 1409939..9db57eb 100644 --- a/src/web/api/photos.py +++ b/backend/api/photos.py @@ -12,14 +12,14 @@ from rq import Queue from redis import Redis from sqlalchemy.orm import Session -from src.web.db.session import get_db -from src.web.api.auth import get_current_user -from src.web.api.users import get_current_admin_user +from backend.db.session import get_db +from backend.api.auth import get_current_user +from backend.api.users import get_current_admin_user # Redis connection for RQ redis_conn = Redis(host="localhost", port=6379, db=0, decode_responses=False) queue = Queue(connection=redis_conn) -from src.web.schemas.photos import ( +from backend.schemas.photos import ( PhotoImportRequest, PhotoImportResponse, PhotoResponse, @@ -30,15 +30,15 @@ from src.web.schemas.photos import ( BulkRemoveFavoritesRequest, BulkRemoveFavoritesResponse, ) -from src.web.schemas.search import ( +from backend.schemas.search import ( PhotoSearchResult, SearchPhotosResponse, ) -from src.web.services.photo_service import ( +from backend.services.photo_service import ( find_photos_in_folder, import_photo_from_path, ) -from src.web.services.search_service import ( +from backend.services.search_service import ( get_favorite_photos, get_photo_face_count, get_photo_person, @@ -83,7 +83,7 @@ def search_photos( - Search unprocessed: returns photos that have not been processed for face detection - Search favorites: returns photos favorited by current user """ - from src.web.db.models import PhotoFavorite + from backend.db.models import PhotoFavorite items: List[PhotoSearchResult] = [] total = 0 @@ -355,7 +355,7 @@ def import_photos( # Enqueue job # Pass function as string path to avoid serialization issues job = queue.enqueue( - "src.web.services.tasks.import_photos_task", + "backend.services.tasks.import_photos_task", request.folder_path, request.recursive, job_timeout="1h", # Allow up to 1 hour for large imports @@ -384,7 +384,7 @@ async def upload_photos( import shutil from pathlib import Path - from src.web.settings import PHOTO_STORAGE_DIR + from backend.settings import PHOTO_STORAGE_DIR # Ensure storage directory exists storage_dir = Path(PHOTO_STORAGE_DIR) @@ -500,7 +500,7 @@ def browse_folder() -> dict: @router.get("/{photo_id}", response_model=PhotoResponse) def get_photo(photo_id: int, db: Session = Depends(get_db)) -> PhotoResponse: """Get photo by ID.""" - from src.web.db.models import Photo + from backend.db.models import Photo photo = db.query(Photo).filter(Photo.id == photo_id).first() if not photo: @@ -517,7 +517,7 @@ def get_photo_image(photo_id: int, db: Session = Depends(get_db)) -> FileRespons """Serve photo image file for display (not download).""" import os import mimetypes - from src.web.db.models import Photo + from backend.db.models import Photo photo = db.query(Photo).filter(Photo.id == photo_id).first() if not photo: @@ -555,7 +555,7 @@ def toggle_favorite( db: Session = Depends(get_db), ) -> dict: """Toggle favorite status of a photo for current user.""" - from src.web.db.models import Photo, PhotoFavorite + from backend.db.models import Photo, PhotoFavorite username = current_user["username"] @@ -602,7 +602,7 @@ def check_favorite( db: Session = Depends(get_db), ) -> dict: """Check if photo is favorited by current user.""" - from src.web.db.models import PhotoFavorite + from backend.db.models import PhotoFavorite username = current_user["username"] @@ -628,7 +628,7 @@ def bulk_add_favorites( Only adds favorites for photos that aren't already favorites. Uses a single database transaction for better performance. """ - from src.web.db.models import Photo, PhotoFavorite + from backend.db.models import Photo, PhotoFavorite photo_ids = request.photo_ids if not photo_ids: @@ -690,7 +690,7 @@ def bulk_remove_favorites( Only removes favorites for photos that are currently favorites. Uses a single database transaction for better performance. """ - from src.web.db.models import Photo, PhotoFavorite + from backend.db.models import Photo, PhotoFavorite photo_ids = request.photo_ids if not photo_ids: @@ -745,7 +745,7 @@ def bulk_delete_photos( db: Session = Depends(get_db), ) -> BulkDeletePhotosResponse: """Delete multiple photos and all related data (faces, encodings, tags, favorites).""" - from src.web.db.models import Photo, PhotoTagLinkage + from backend.db.models import Photo, PhotoTagLinkage photo_ids = list(dict.fromkeys(request.photo_ids)) if not photo_ids: @@ -804,7 +804,7 @@ def open_photo_folder(photo_id: int, db: Session = Depends(get_db)) -> dict: import os import sys import subprocess - from src.web.db.models import Photo + from backend.db.models import Photo photo = db.query(Photo).filter(Photo.id == photo_id).first() if not photo: diff --git a/src/web/api/reported_photos.py b/backend/api/reported_photos.py similarity index 98% rename from src/web/api/reported_photos.py rename to backend/api/reported_photos.py index e54108d..6d814e3 100644 --- a/src/web/api/reported_photos.py +++ b/backend/api/reported_photos.py @@ -10,9 +10,9 @@ from pydantic import BaseModel, ConfigDict from sqlalchemy import text from sqlalchemy.orm import Session -from src.web.db.session import get_auth_db, get_db -from src.web.db.models import Photo, PhotoTagLinkage -from src.web.api.users import get_current_admin_user, require_feature_permission +from backend.db.session import get_auth_db, get_db +from backend.db.models import Photo, PhotoTagLinkage +from backend.api.users import get_current_admin_user, require_feature_permission router = APIRouter(prefix="/reported-photos", tags=["reported-photos"]) diff --git a/src/web/api/role_permissions.py b/backend/api/role_permissions.py similarity index 87% rename from src/web/api/role_permissions.py rename to backend/api/role_permissions.py index bef4d24..63825dd 100644 --- a/src/web/api/role_permissions.py +++ b/backend/api/role_permissions.py @@ -7,16 +7,16 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from src.web.api.users import get_current_admin_user -from src.web.constants.role_features import ROLE_FEATURES, ROLE_FEATURE_KEYS -from src.web.constants.roles import ROLE_VALUES -from src.web.db.session import get_db -from src.web.schemas.role_permissions import ( +from backend.api.users import get_current_admin_user +from backend.constants.role_features import ROLE_FEATURES, ROLE_FEATURE_KEYS +from backend.constants.roles import ROLE_VALUES +from backend.db.session import get_db +from backend.schemas.role_permissions import ( RoleFeatureSchema, RolePermissionsResponse, RolePermissionsUpdateRequest, ) -from src.web.services.role_permissions import ( +from backend.services.role_permissions import ( ensure_role_permissions_initialized, fetch_role_permissions_map, set_role_permissions, @@ -68,3 +68,7 @@ def update_role_permissions( + + + + diff --git a/src/web/api/tags.py b/backend/api/tags.py similarity index 97% rename from src/web/api/tags.py rename to backend/api/tags.py index bff8712..748dd4a 100644 --- a/src/web/api/tags.py +++ b/backend/api/tags.py @@ -7,8 +7,8 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from src.web.db.session import get_db -from src.web.schemas.tags import ( +from backend.db.session import get_db +from backend.schemas.tags import ( PhotoTagsRequest, PhotoTagsResponse, TagCreateRequest, @@ -21,7 +21,7 @@ from src.web.schemas.tags import ( PhotosWithTagsResponse, PhotoWithTagsItem, ) -from src.web.services.tag_service import ( +from backend.services.tag_service import ( add_tags_to_photos, get_or_create_tag, list_tags, @@ -31,7 +31,7 @@ from src.web.services.tag_service import ( delete_tags, get_photos_with_tags, ) -from src.web.db.models import Photo +from backend.db.models import Photo router = APIRouter(prefix="/tags", tags=["tags"]) diff --git a/src/web/api/users.py b/backend/api/users.py similarity index 98% rename from src/web/api/users.py rename to backend/api/users.py index f524d8e..1bb55e8 100644 --- a/src/web/api/users.py +++ b/backend/api/users.py @@ -11,24 +11,24 @@ from sqlalchemy import text from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from src.web.api.auth import get_current_user -from src.web.constants.roles import ( +from backend.api.auth import get_current_user +from backend.constants.roles import ( DEFAULT_ADMIN_ROLE, DEFAULT_USER_ROLE, ROLE_VALUES, UserRole, is_admin_role, ) -from src.web.db.session import get_auth_db, get_db -from src.web.db.models import Face, PhotoFavorite, PhotoPersonLinkage, User -from src.web.schemas.users import ( +from backend.db.session import get_auth_db, get_db +from backend.db.models import Face, PhotoFavorite, PhotoPersonLinkage, User +from backend.schemas.users import ( UserCreateRequest, UserResponse, UserUpdateRequest, UsersListResponse, ) -from src.web.utils.password import hash_password -from src.web.services.role_permissions import fetch_role_permissions_map +from backend.utils.password import hash_password +from backend.services.role_permissions import fetch_role_permissions_map router = APIRouter(prefix="/users", tags=["users"]) logger = logging.getLogger(__name__) diff --git a/src/web/api/version.py b/backend/api/version.py similarity index 84% rename from src/web/api/version.py rename to backend/api/version.py index e78bf30..82079ca 100644 --- a/src/web/api/version.py +++ b/backend/api/version.py @@ -2,7 +2,7 @@ from __future__ import annotations from fastapi import APIRouter -from src.web.settings import APP_VERSION +from backend.settings import APP_VERSION router = APIRouter() diff --git a/src/web/api/videos.py b/backend/api/videos.py similarity index 97% rename from src/web/api/videos.py rename to backend/api/videos.py index beebe07..55c7d7a 100644 --- a/src/web/api/videos.py +++ b/backend/api/videos.py @@ -9,10 +9,10 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session -from src.web.db.session import get_db -from src.web.db.models import Photo, User -from src.web.api.auth import get_current_user_with_id -from src.web.schemas.videos import ( +from backend.db.session import get_db +from backend.db.models import Photo, User +from backend.api.auth import get_current_user_with_id +from backend.schemas.videos import ( ListVideosResponse, VideoListItem, PersonInfo, @@ -22,14 +22,14 @@ from src.web.schemas.videos import ( IdentifyVideoResponse, RemoveVideoPersonResponse, ) -from src.web.services.video_service import ( +from backend.services.video_service import ( list_videos_for_identification, get_video_people, identify_person_in_video, remove_person_from_video, get_video_people_count, ) -from src.web.services.thumbnail_service import get_video_thumbnail_path +from backend.services.thumbnail_service import get_video_thumbnail_path router = APIRouter(prefix="/videos", tags=["videos"]) @@ -337,3 +337,7 @@ def get_video_file( + + + + diff --git a/src/web/app.py b/backend/app.py similarity index 70% rename from src/web/app.py rename to backend/app.py index f2abe6c..149b513 100644 --- a/src/web/app.py +++ b/backend/app.py @@ -10,31 +10,31 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import inspect, text -from src.web.api.auth import router as auth_router -from src.web.api.faces import router as faces_router -from src.web.api.health import router as health_router -from src.web.api.jobs import router as jobs_router -from src.web.api.metrics import router as metrics_router -from src.web.api.people import router as people_router -from src.web.api.pending_identifications import router as pending_identifications_router -from src.web.api.pending_linkages import router as pending_linkages_router -from src.web.api.photos import router as photos_router -from src.web.api.reported_photos import router as reported_photos_router -from src.web.api.pending_photos import router as pending_photos_router -from src.web.api.tags import router as tags_router -from src.web.api.users import router as users_router -from src.web.api.auth_users import router as auth_users_router -from src.web.api.role_permissions import router as role_permissions_router -from src.web.api.videos import router as videos_router -from src.web.api.version import router as version_router -from src.web.settings import APP_TITLE, APP_VERSION -from src.web.constants.roles import DEFAULT_ADMIN_ROLE, DEFAULT_USER_ROLE, ROLE_VALUES -from src.web.db.base import Base, engine -from src.web.db.session import auth_engine, database_url +from backend.api.auth import router as auth_router +from backend.api.faces import router as faces_router +from backend.api.health import router as health_router +from backend.api.jobs import router as jobs_router +from backend.api.metrics import router as metrics_router +from backend.api.people import router as people_router +from backend.api.pending_identifications import router as pending_identifications_router +from backend.api.pending_linkages import router as pending_linkages_router +from backend.api.photos import router as photos_router +from backend.api.reported_photos import router as reported_photos_router +from backend.api.pending_photos import router as pending_photos_router +from backend.api.tags import router as tags_router +from backend.api.users import router as users_router +from backend.api.auth_users import router as auth_users_router +from backend.api.role_permissions import router as role_permissions_router +from backend.api.videos import router as videos_router +from backend.api.version import router as version_router +from backend.settings import APP_TITLE, APP_VERSION +from backend.constants.roles import DEFAULT_ADMIN_ROLE, DEFAULT_USER_ROLE, ROLE_VALUES +from backend.db.base import Base, engine +from backend.db.session import auth_engine, database_url, get_auth_database_url # Import models to ensure they're registered with Base.metadata -from src.web.db import models # noqa: F401 -from src.web.db.models import RolePermission -from src.web.utils.password import hash_password +from backend.db import models # noqa: F401 +from backend.db.models import RolePermission +from backend.utils.password import hash_password # Global worker process (will be set in lifespan) _worker_process: subprocess.Popen | None = None @@ -52,22 +52,31 @@ def start_worker() -> None: redis_conn.ping() # Start worker as a subprocess (avoids signal handler issues) - project_root = Path(__file__).parent.parent.parent + # __file__ is backend/app.py, so parent.parent is the project root (punimtag/) + project_root = Path(__file__).parent.parent + + # Use explicit Python path to avoid Cursor interception + # Check if sys.executable is Cursor, if so use /usr/bin/python3 python_executable = sys.executable + if "cursor" in python_executable.lower() or not python_executable.startswith("/usr"): + python_executable = "/usr/bin/python3" + + # Ensure PYTHONPATH is set correctly + worker_env = { + **{k: v for k, v in os.environ.items()}, + "PYTHONPATH": str(project_root), + } _worker_process = subprocess.Popen( [ python_executable, "-m", - "src.web.worker", + "backend.worker", ], cwd=str(project_root), stdout=None, # Don't capture - let output go to console stderr=None, # Don't capture - let errors go to console - env={ - **{k: v for k, v in os.environ.items()}, - "PYTHONPATH": str(project_root), - } + env=worker_env ) # Give it a moment to start, then check if it's still running import time @@ -378,7 +387,7 @@ def ensure_face_excluded_column(inspector) -> None: columns = {column["name"] for column in inspector.get_columns("faces")} if "excluded" in columns: - print("ℹ️ excluded column already exists in faces table") + # Column already exists, no need to print or do anything return print("🔄 Adding excluded column to faces table...") @@ -476,17 +485,32 @@ def ensure_photo_person_linkage_table(inspector) -> None: def ensure_auth_user_is_active_column() -> None: - """Ensure auth database users table contains is_active column.""" + """Ensure auth database users table contains is_active column. + + NOTE: Auth database is managed by the frontend. This function only checks/updates + if the database and table already exist. It will not fail if they don't exist. + """ if auth_engine is None: # Auth database not configured return try: from sqlalchemy import inspect as sqlalchemy_inspect - auth_inspector = sqlalchemy_inspect(auth_engine) + + # Try to get inspector - gracefully handle if database doesn't exist + try: + auth_inspector = sqlalchemy_inspect(auth_engine) + except Exception as inspect_exc: + error_str = str(inspect_exc).lower() + if "does not exist" in error_str or "database" in error_str: + # Database doesn't exist - that's okay, frontend will create it + return + # Some other error - log but don't fail + print(f"ℹ️ Could not inspect auth database: {inspect_exc}") + return if "users" not in auth_inspector.get_table_names(): - print("ℹ️ Auth database users table does not exist yet") + # Table doesn't exist - that's okay, frontend will create it return columns = {column["name"] for column in auth_inspector.get_columns("users")} @@ -545,11 +569,170 @@ def ensure_role_permissions_table(inspector) -> None: print(f"⚠️ Failed to create role_permissions table: {exc}") -@asynccontextmanager +def ensure_postgresql_database(db_url: str) -> None: + """Ensure PostgreSQL database exists, create it if it doesn't.""" + if not db_url.startswith("postgresql"): + return # Not PostgreSQL, skip + + try: + from urllib.parse import urlparse, parse_qs + import os + import psycopg2 + from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + + # Parse the database URL + parsed = urlparse(db_url.replace("postgresql+psycopg2://", "postgresql://")) + db_name = parsed.path.lstrip("/") + user = parsed.username + password = parsed.password + host = parsed.hostname or "localhost" + port = parsed.port or 5432 + + if not db_name: + return # No database name specified + + # Try to connect to the database + try: + test_conn = psycopg2.connect( + host=host, + port=port, + user=user, + password=password, + database=db_name + ) + test_conn.close() + return # Database exists + except psycopg2.OperationalError as e: + if "does not exist" not in str(e): + # Some other error (permissions, connection, etc.) + print(f"⚠️ Cannot check if database '{db_name}' exists: {e}") + return + + # Database doesn't exist - try to create it + print(f"🔄 Creating PostgreSQL database '{db_name}'...") + + # Connect to postgres database to create the new database + # Try with the configured user first (they might have CREATEDB privilege) + admin_conn = None + try: + admin_conn = psycopg2.connect( + host=host, + port=port, + user=user, + password=password, + database="postgres" + ) + except psycopg2.OperationalError: + # Try postgres superuser (might need password from environment or .pgpass) + try: + import os + postgres_password = os.getenv("POSTGRES_PASSWORD", "") + admin_conn = psycopg2.connect( + host=host, + port=port, + user="postgres", + password=postgres_password if postgres_password else None, + database="postgres" + ) + except psycopg2.OperationalError as e: + print(f"⚠️ Cannot create database '{db_name}': insufficient privileges") + print(f" Error: {e}") + print(f" Please create it manually:") + print(f" sudo -u postgres psql -c \"CREATE DATABASE {db_name};\"") + print(f" sudo -u postgres psql -c \"GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {user};\"") + return + + if admin_conn is None: + return + + admin_conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cursor = admin_conn.cursor() + + # Check if database exists + cursor.execute( + "SELECT 1 FROM pg_database WHERE datname = %s", + (db_name,) + ) + exists = cursor.fetchone() + + if not exists: + # Create the database + try: + cursor.execute(f'CREATE DATABASE "{db_name}"') + if user != "postgres" and admin_conn.info.user == "postgres": + # Grant privileges to the user if we're connected as postgres + try: + cursor.execute(f'GRANT ALL PRIVILEGES ON DATABASE "{db_name}" TO "{user}"') + except Exception as grant_exc: + print(f"⚠️ Created database '{db_name}' but could not grant privileges: {grant_exc}") + + # Grant schema permissions (needed for creating tables) + if admin_conn.info.user == "postgres": + try: + # Connect to the new database to grant schema permissions + cursor.close() + admin_conn.close() + schema_conn = psycopg2.connect( + host=host, + port=port, + user="postgres", + password=os.getenv("POSTGRES_PASSWORD", "") if os.getenv("POSTGRES_PASSWORD") else None, + database=db_name + ) + schema_conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + schema_cursor = schema_conn.cursor() + schema_cursor.execute(f'GRANT ALL ON SCHEMA public TO "{user}"') + schema_cursor.execute(f'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "{user}"') + schema_cursor.execute(f'ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "{user}"') + schema_cursor.close() + schema_conn.close() + print(f"✅ Granted schema permissions to user '{user}'") + except Exception as schema_exc: + print(f"⚠️ Created database '{db_name}' but could not grant schema permissions: {schema_exc}") + print(f" Please run manually:") + print(f" sudo -u postgres psql -d {db_name} -c \"GRANT ALL ON SCHEMA public TO {user};\"") + print(f" sudo -u postgres psql -d {db_name} -c \"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO {user};\"") + + print(f"✅ Created database '{db_name}'") + except Exception as create_exc: + print(f"⚠️ Failed to create database '{db_name}': {create_exc}") + print(f" Please create it manually:") + print(f" sudo -u postgres psql -c \"CREATE DATABASE {db_name};\"") + if user != "postgres": + print(f" sudo -u postgres psql -c \"GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {user};\"") + cursor.close() + admin_conn.close() + return + else: + print(f"ℹ️ Database '{db_name}' already exists") + + cursor.close() + admin_conn.close() + except Exception as exc: + print(f"⚠️ Failed to ensure database exists: {exc}") + import traceback + print(f" Traceback: {traceback.format_exc()}") + # Don't raise - let the connection attempt fail naturally with a clearer error + + +def ensure_auth_database_tables() -> None: + """Ensure auth database tables exist, create them if they don't. + + NOTE: This function is deprecated. Auth database is now managed by the frontend. + This function is kept for backward compatibility but will not create tables. + """ + # Auth database is managed by the frontend - do not create tables here + return async def lifespan(app: FastAPI): """Lifespan context manager for startup and shutdown events.""" # Ensure database exists and tables are created on first run try: + # Ensure main PostgreSQL database exists + # This must happen BEFORE we try to use the engine + ensure_postgresql_database(database_url) + + # Note: Auth database is managed by the frontend, not created here + if database_url.startswith("sqlite"): db_path = database_url.replace("sqlite:///", "") db_file = Path(db_path) @@ -586,8 +769,15 @@ async def lifespan(app: FastAPI): ensure_face_excluded_column(inspector) ensure_role_permissions_table(inspector) - # Ensure auth database schema - ensure_auth_user_is_active_column() + # Note: Auth database schema and tables are managed by the frontend + # Only check/update if the database exists (don't create it) + if auth_engine is not None: + try: + ensure_auth_user_is_active_column() + except Exception as auth_exc: + # Auth database might not exist yet - that's okay, frontend will handle it + print(f"ℹ️ Auth database not available: {auth_exc}") + print(" Frontend will manage auth database setup") except Exception as exc: print(f"❌ Database initialization failed: {exc}") raise diff --git a/src/web/config.py b/backend/config.py similarity index 100% rename from src/web/config.py rename to backend/config.py diff --git a/src/web/constants/role_features.py b/backend/constants/role_features.py similarity index 97% rename from src/web/constants/role_features.py rename to backend/constants/role_features.py index 8e12f78..2af0757 100644 --- a/src/web/constants/role_features.py +++ b/backend/constants/role_features.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Dict, Final, List, Set -from src.web.constants.roles import UserRole +from backend.constants.roles import UserRole ROLE_FEATURES: Final[List[dict[str, str]]] = [ {"key": "scan", "label": "Scan"}, diff --git a/src/web/constants/roles.py b/backend/constants/roles.py similarity index 100% rename from src/web/constants/roles.py rename to backend/constants/roles.py diff --git a/src/web/db/__init__.py b/backend/db/__init__.py similarity index 100% rename from src/web/db/__init__.py rename to backend/db/__init__.py diff --git a/src/web/db/base.py b/backend/db/base.py similarity index 58% rename from src/web/db/base.py rename to backend/db/base.py index d64ccdf..7f2ee55 100644 --- a/src/web/db/base.py +++ b/backend/db/base.py @@ -2,8 +2,8 @@ from __future__ import annotations -from src.web.db.models import Base -from src.web.db.session import engine +from backend.db.models import Base +from backend.db.session import engine __all__ = ["Base", "engine"] diff --git a/src/web/db/models.py b/backend/db/models.py similarity index 99% rename from src/web/db/models.py rename to backend/db/models.py index 52ad214..a534f30 100644 --- a/src/web/db/models.py +++ b/backend/db/models.py @@ -21,7 +21,7 @@ from sqlalchemy import ( ) from sqlalchemy.orm import declarative_base, relationship -from src.web.constants.roles import DEFAULT_USER_ROLE +from backend.constants.roles import DEFAULT_USER_ROLE if TYPE_CHECKING: pass diff --git a/src/web/db/session.py b/backend/db/session.py similarity index 100% rename from src/web/db/session.py rename to backend/db/session.py diff --git a/src/web/schemas/__init__.py b/backend/schemas/__init__.py similarity index 100% rename from src/web/schemas/__init__.py rename to backend/schemas/__init__.py diff --git a/src/web/schemas/auth.py b/backend/schemas/auth.py similarity index 95% rename from src/web/schemas/auth.py rename to backend/schemas/auth.py index 368909f..aa38815 100644 --- a/src/web/schemas/auth.py +++ b/backend/schemas/auth.py @@ -6,7 +6,7 @@ from typing import Dict from pydantic import BaseModel, ConfigDict -from src.web.constants.roles import DEFAULT_USER_ROLE, UserRole +from backend.constants.roles import DEFAULT_USER_ROLE, UserRole class LoginRequest(BaseModel): diff --git a/src/web/schemas/auth_users.py b/backend/schemas/auth_users.py similarity index 100% rename from src/web/schemas/auth_users.py rename to backend/schemas/auth_users.py diff --git a/src/web/schemas/faces.py b/backend/schemas/faces.py similarity index 100% rename from src/web/schemas/faces.py rename to backend/schemas/faces.py diff --git a/src/web/schemas/jobs.py b/backend/schemas/jobs.py similarity index 100% rename from src/web/schemas/jobs.py rename to backend/schemas/jobs.py diff --git a/src/web/schemas/people.py b/backend/schemas/people.py similarity index 100% rename from src/web/schemas/people.py rename to backend/schemas/people.py diff --git a/src/web/schemas/photos.py b/backend/schemas/photos.py similarity index 100% rename from src/web/schemas/photos.py rename to backend/schemas/photos.py diff --git a/src/web/schemas/role_permissions.py b/backend/schemas/role_permissions.py similarity index 90% rename from src/web/schemas/role_permissions.py rename to backend/schemas/role_permissions.py index d8c5b8b..be1303b 100644 --- a/src/web/schemas/role_permissions.py +++ b/backend/schemas/role_permissions.py @@ -6,8 +6,8 @@ from typing import Dict from pydantic import BaseModel, ConfigDict, Field -from src.web.constants.role_features import ROLE_FEATURES -from src.web.constants.roles import UserRole +from backend.constants.role_features import ROLE_FEATURES +from backend.constants.roles import UserRole class RoleFeatureSchema(BaseModel): @@ -42,3 +42,7 @@ class RolePermissionsUpdateRequest(BaseModel): + + + + diff --git a/src/web/schemas/search.py b/backend/schemas/search.py similarity index 100% rename from src/web/schemas/search.py rename to backend/schemas/search.py diff --git a/src/web/schemas/tags.py b/backend/schemas/tags.py similarity index 100% rename from src/web/schemas/tags.py rename to backend/schemas/tags.py diff --git a/src/web/schemas/users.py b/backend/schemas/users.py similarity index 97% rename from src/web/schemas/users.py rename to backend/schemas/users.py index 5547e34..e99e1e9 100644 --- a/src/web/schemas/users.py +++ b/backend/schemas/users.py @@ -7,7 +7,7 @@ from typing import Optional from pydantic import BaseModel, ConfigDict, EmailStr, Field -from src.web.constants.roles import DEFAULT_USER_ROLE, UserRole +from backend.constants.roles import DEFAULT_USER_ROLE, UserRole class UserResponse(BaseModel): diff --git a/src/web/schemas/videos.py b/backend/schemas/videos.py similarity index 99% rename from src/web/schemas/videos.py rename to backend/schemas/videos.py index d78d0de..50f142b 100644 --- a/src/web/schemas/videos.py +++ b/backend/schemas/videos.py @@ -90,3 +90,7 @@ class RemoveVideoPersonResponse(BaseModel): + + + + diff --git a/src/web/services/__init__.py b/backend/services/__init__.py similarity index 100% rename from src/web/services/__init__.py rename to backend/services/__init__.py diff --git a/src/web/services/face_service.py b/backend/services/face_service.py similarity index 99% rename from src/web/services/face_service.py rename to backend/services/face_service.py index 619c436..df5eb84 100644 --- a/src/web/services/face_service.py +++ b/backend/services/face_service.py @@ -20,7 +20,7 @@ try: except ImportError: DEEPFACE_AVAILABLE = False -from src.web.config import ( +from backend.config import ( CONFIDENCE_CALIBRATION_METHOD, DEFAULT_FACE_TOLERANCE, DEEPFACE_ALIGN_FACES, @@ -32,7 +32,7 @@ from src.web.config import ( ) from src.utils.exif_utils import EXIFOrientationHandler from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE -from src.web.db.models import Face, Photo, Person, Tag, PhotoTagLinkage +from backend.db.models import Face, Photo, Person, Tag, PhotoTagLinkage def _pre_warm_deepface( @@ -1531,7 +1531,7 @@ def find_similar_faces( filter_frontal_only: Only return frontal or tilted faces (not profile) include_excluded: Include excluded faces in results (default: False) """ - from src.web.db.models import Photo + from backend.db.models import Photo if tolerance is None: tolerance = DEFAULT_FACE_TOLERANCE @@ -1987,7 +1987,7 @@ def get_auto_match_person_matches( Returns: List of (face, distance, confidence_pct) tuples """ - from src.web.db.models import Person, Face + from backend.db.models import Person, Face # Get reference face for this person (best quality >= 0.3) reference_face = ( @@ -2034,7 +2034,7 @@ def accept_auto_match_matches( Returns: (identified_count, updated_count) tuple """ - from src.web.db.models import PersonEncoding + from backend.db.models import PersonEncoding # Validate person exists person = db.query(Person).filter(Person.id == person_id).first() diff --git a/src/web/services/photo_service.py b/backend/services/photo_service.py similarity index 99% rename from src/web/services/photo_service.py rename to backend/services/photo_service.py index 029a0d6..4d943c8 100644 --- a/src/web/services/photo_service.py +++ b/backend/services/photo_service.py @@ -11,8 +11,8 @@ from typing import Callable, Optional, Tuple from PIL import Image from sqlalchemy.orm import Session -from src.web.config import SUPPORTED_IMAGE_FORMATS, SUPPORTED_VIDEO_FORMATS -from src.web.db.models import Photo +from backend.config import SUPPORTED_IMAGE_FORMATS, SUPPORTED_VIDEO_FORMATS +from backend.db.models import Photo def extract_exif_date(image_path: str) -> Optional[date]: diff --git a/src/web/services/role_permissions.py b/backend/services/role_permissions.py similarity index 93% rename from src/web/services/role_permissions.py rename to backend/services/role_permissions.py index d705592..ee9dbeb 100644 --- a/src/web/services/role_permissions.py +++ b/backend/services/role_permissions.py @@ -8,12 +8,12 @@ from typing import Dict from sqlalchemy import select from sqlalchemy.orm import Session -from src.web.constants.role_features import ( +from backend.constants.role_features import ( ROLE_FEATURE_KEYS, get_default_permission, ) -from src.web.constants.roles import ROLE_VALUES -from src.web.db.models import RolePermission +from backend.constants.roles import ROLE_VALUES +from backend.db.models import RolePermission def ensure_role_permissions_initialized(session: Session) -> None: diff --git a/src/web/services/search_service.py b/backend/services/search_service.py similarity index 99% rename from src/web/services/search_service.py rename to backend/services/search_service.py index a9e77f2..99bd520 100644 --- a/src/web/services/search_service.py +++ b/backend/services/search_service.py @@ -8,7 +8,7 @@ from typing import List, Optional, Tuple from sqlalchemy import func, or_ from sqlalchemy.orm import Session -from src.web.db.models import Face, Photo, Person, PhotoTagLinkage, Tag +from backend.db.models import Face, Photo, Person, PhotoTagLinkage, Tag def search_photos_by_name( @@ -458,7 +458,7 @@ def get_favorite_photos( Filters by media_type if provided ("image" or "video", None/"all" for all). """ - from src.web.db.models import PhotoFavorite + from backend.db.models import PhotoFavorite # Join favorites with photos query = ( diff --git a/src/web/services/tag_service.py b/backend/services/tag_service.py similarity index 99% rename from src/web/services/tag_service.py rename to backend/services/tag_service.py index c93f370..005973f 100644 --- a/src/web/services/tag_service.py +++ b/backend/services/tag_service.py @@ -7,7 +7,7 @@ from datetime import datetime from sqlalchemy.orm import Session -from src.web.db.models import Photo, PhotoTagLinkage, Tag, Face, Person +from backend.db.models import Photo, PhotoTagLinkage, Tag, Face, Person def list_tags(db: Session) -> List[Tag]: diff --git a/src/web/services/tasks.py b/backend/services/tasks.py similarity index 98% rename from src/web/services/tasks.py rename to backend/services/tasks.py index 1b0dc43..961b1c4 100644 --- a/src/web/services/tasks.py +++ b/backend/services/tasks.py @@ -7,9 +7,9 @@ from typing import Optional from rq import get_current_job from sqlalchemy.orm import Session -from src.web.db.session import SessionLocal -from src.web.services.photo_service import import_photos_from_folder -from src.web.services.face_service import process_unprocessed_photos +from backend.db.session import SessionLocal +from backend.services.photo_service import import_photos_from_folder +from backend.services.face_service import process_unprocessed_photos def import_photos_task(folder_path: str, recursive: bool = True) -> dict: diff --git a/src/web/services/thumbnail_service.py b/backend/services/thumbnail_service.py similarity index 100% rename from src/web/services/thumbnail_service.py rename to backend/services/thumbnail_service.py diff --git a/src/web/services/video_service.py b/backend/services/video_service.py similarity index 99% rename from src/web/services/video_service.py rename to backend/services/video_service.py index 411e6d5..b73423b 100644 --- a/src/web/services/video_service.py +++ b/backend/services/video_service.py @@ -8,7 +8,7 @@ from typing import List, Optional, Tuple from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session -from src.web.db.models import Photo, Person, PhotoPersonLinkage, User +from backend.db.models import Photo, Person, PhotoPersonLinkage, User def list_videos_for_identification( diff --git a/src/web/settings.py b/backend/settings.py similarity index 100% rename from src/web/settings.py rename to backend/settings.py diff --git a/src/web/utils/__init__.py b/backend/utils/__init__.py similarity index 100% rename from src/web/utils/__init__.py rename to backend/utils/__init__.py diff --git a/src/web/utils/password.py b/backend/utils/password.py similarity index 100% rename from src/web/utils/password.py rename to backend/utils/password.py diff --git a/src/web/worker.py b/backend/worker.py similarity index 95% rename from src/web/worker.py rename to backend/worker.py index 3aacdd2..40fda03 100644 --- a/src/web/worker.py +++ b/backend/worker.py @@ -11,7 +11,7 @@ import uuid from rq import Worker from redis import Redis -from src.web.services.tasks import import_photos_task, process_faces_task +from backend.services.tasks import import_photos_task, process_faces_task # Redis connection for RQ redis_conn = Redis(host="localhost", port=6379, db=0, decode_responses=False) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index dd7df0e..abe1933 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -6,7 +6,7 @@ services: working_dir: /app volumes: - ..:/app - command: bash -lc "pip install -r requirements.txt && uvicorn src.web.app:app --host 0.0.0.0 --port 8000" + command: bash -lc "pip install -r requirements.txt && uvicorn backend.app:app --host 0.0.0.0 --port 8000" environment: - DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/punimtag - PYTHONUNBUFFERED=1 @@ -21,7 +21,7 @@ services: working_dir: /app volumes: - ..:/app - command: bash -lc "pip install -r requirements.txt && python -m src.web.worker" + command: bash -lc "pip install -r requirements.txt && python -m backend.worker" environment: - DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/punimtag - PYTHONUNBUFFERED=1 diff --git a/docs/CLIENT_DEPLOYMENT_QUESTIONS.md b/docs/CLIENT_DEPLOYMENT_QUESTIONS.md new file mode 100644 index 0000000..a16abc3 --- /dev/null +++ b/docs/CLIENT_DEPLOYMENT_QUESTIONS.md @@ -0,0 +1,219 @@ +# Client Deployment Questions + +**PunimTag Web Application - Information Needed for Deployment** + +We have the source code ready. To deploy on your server, we need the following information: + +--- + +## 1. Server Access + +**How can we access your server?** +- [ ] SSH access + - Server IP/hostname: `_________________` + - SSH port: `_________________` (default: 22) + - Username: `_________________` + - Authentication method: + - [ ] SSH key (provide public key or key file) + - [ ] Username/password: `_________________` +- [ ] Other access method: `_________________` + +**Do we have permission to install software?** +- [ ] Yes, we can install packages +- [ ] No, limited permissions (what can we do?): `_________________` + +--- + +## 2. Databases + +**We need TWO PostgreSQL databases:** + +### Main Database (for photos, faces, people, tags) +- **Database server location:** + - [ ] Same server as application + - [ ] Different server: `_________________` +- **Connection details:** + - Host/IP: `_________________` + - Port: `_________________` (default: 5432) + - Database name: `_________________` (or we can create: `punimtag`) + - Username: `_________________` + - Password: `_________________` +- **Can we create the database?** + - [ ] Yes + - [ ] No (provide existing database details above) + +### Auth Database (for frontend website user accounts) +- **Database server location:** + - [ ] Same server as main database + - [ ] Same server as application (different database) + - [ ] Different server: `_________________` +- **Connection details:** + - Host/IP: `_________________` + - Port: `_________________` (default: 5432) + - Database name: `_________________` (or we can create: `punimtag_auth`) + - Username: `_________________` + - Password: `_________________` +- **Can we create the database?** + - [ ] Yes + - [ ] No (provide existing database details above) + +**Database access:** +- Can the application server connect to the databases? + - [ ] Yes, direct connection + - [ ] VPN required: `_________________` + - [ ] IP whitelist required: `_________________` + +--- + +## 3. Redis (for background jobs) + +**Redis server:** +- [ ] Same server as application +- [ ] Different server: `_________________` +- [ ] Not installed (we can install) + +**If separate server:** +- Host/IP: `_________________` +- Port: `_________________` (default: 6379) +- Password (if required): `_________________` + +--- + +## 4. Network & Ports + +**What ports can we use?** +- Backend API (port 8000): + - [ ] Can use port 8000 + - [ ] Need different port: `_________________` +- Frontend (port 3000 for dev, or web server for production): + - [ ] Can use port 3000 + - [ ] Need different port: `_________________` + - [ ] Will use web server (Nginx/Apache) - port 80/443 + +**Who needs to access the application?** +- [ ] Internal network only +- [ ] External users (internet) +- [ ] VPN users only +- [ ] Specific IP ranges: `_________________` + +**Domain/URL:** +- Do you have a domain name? `_________________` +- What URL should users access? `_________________` (e.g., `https://punimtag.yourdomain.com`) + +**Firewall:** +- [ ] We can configure firewall rules +- [ ] IT team manages firewall (contact: `_________________`) + +--- + +## 5. Frontend Website + +**How should the frontend be served?** +- [ ] Development mode (Vite dev server) +- [ ] Production build with web server (Nginx/Apache) +- [ ] Other: `_________________` + +**Backend API URL for frontend:** +- What URL should the frontend use to connect to the backend API? + - `_________________` (e.g., `http://server-ip:8000` or `https://api.yourdomain.com`) +- **Important:** This URL must be accessible from users' browsers (not just localhost) + +**Web server (if using production build):** +- [ ] Nginx installed +- [ ] Apache installed +- [ ] Not installed (we can install/configure) +- [ ] Other: `_________________` + +--- + +## 6. Storage + +**Where should uploaded photos be stored?** +- Storage path: `_________________` (e.g., `/var/punimtag/photos` or `/data/uploads`) +- [ ] We can create and configure the directory +- [ ] Directory already exists: `_________________` + +**Storage type:** +- [ ] Local disk +- [ ] Network storage (NAS): `_________________` +- [ ] Other: `_________________` + +--- + +## 7. Software Installation + +**What's already installed on the server?** +- Python 3.12+: [ ] Yes [ ] No +- Node.js 18+: [ ] Yes [ ] No +- PostgreSQL: [ ] Yes [ ] No +- Redis: [ ] Yes [ ] No +- Git: [ ] Yes [ ] No + +**Can we install missing software?** +- [ ] Yes +- [ ] No (what's available?): `_________________` + +**Does the server have internet access?** +- [ ] Yes (can download packages) +- [ ] No (internal package repository?): `_________________` + +--- + +## 8. SSL/HTTPS + +**Do you need HTTPS?** +- [ ] Yes (SSL certificate required) + - [ ] We can generate self-signed certificate + - [ ] You will provide certificate + - [ ] Let's Encrypt (domain required) +- [ ] No (HTTP is fine for testing) + +--- + +## 9. Code Deployment + +**How should we deploy the code?** +- [ ] Git repository access + - Repository URL: `_________________` + - Access credentials: `_________________` +- [ ] File transfer (SFTP/SCP) +- [ ] We will provide deployment package +- [ ] Other: `_________________` + +--- + +## 10. Contact Information + +**Who should we contact for:** +- IT/Network issues: `_________________` (email: `_________________`, phone: `_________________`) +- Database issues: `_________________` (email: `_________________`, phone: `_________________`) +- General questions: `_________________` (email: `_________________`, phone: `_________________`) + +--- + +## Quick Summary + +**What we need:** +1. ✅ Server access (SSH) +2. ✅ Two PostgreSQL databases (main + auth) +3. ✅ Redis server +4. ✅ Network ports (8000 for API, 3000 or web server for frontend) +5. ✅ Storage location for photos +6. ✅ Frontend API URL configuration +7. ✅ Contact information + +**What we'll do:** +- Install required software (if needed) +- Configure databases +- Deploy and configure the application +- Set up frontend website +- Test everything works + +--- + +**Please fill out this form and return it to us so we can begin deployment.** + + + + + diff --git a/docs/CLIENT_NETWORK_TESTING_INFO_for_BOTH.md b/docs/CLIENT_NETWORK_TESTING_INFO_for_BOTH.md new file mode 100644 index 0000000..3ff1506 --- /dev/null +++ b/docs/CLIENT_NETWORK_TESTING_INFO_for_BOTH.md @@ -0,0 +1,505 @@ +# Client Network Testing Information Request + +**PunimTag Web Application - Network Testing Setup** + +This document outlines the information required from your organization to begin testing the PunimTag web application on your network infrastructure. + +--- + +## 1. Server Access & Infrastructure + +### 1.1 Server Details +- **Server Hostname/IP Address**: `_________________` +- **Operating System**: `_________________` (e.g., Ubuntu 22.04, RHEL 9, Windows Server 2022) +- **SSH Access Method**: + - [ ] SSH Key-based authentication (provide public key) + - [ ] Username/Password authentication +- **SSH Port**: `_________________` (default: 22) +- **SSH Username**: `_________________` +- **SSH Credentials**: `_________________` (or key file location) +- **Sudo/Root Access**: + - [ ] Yes (required for service installation) + - [ ] No (limited permissions - specify what's available) + +### 1.2 Server Specifications +- **CPU**: `_________________` (cores/threads) +- **RAM**: `_________________` GB +- **Disk Space Available**: `_________________` GB +- **Network Bandwidth**: `_________________` Mbps +- **Is this a virtual machine or physical server?**: `_________________` + +--- + +## 2. Network Configuration + +### 2.1 Network Topology +- **Network Type**: + - [ ] Internal/Private network only + - [ ] Internet-facing with public IP + - [ ] VPN-accessible only + - [ ] Hybrid (internal + external access) + +### 2.2 IP Addresses & Ports +- **Server IP Address**: `_________________` +- **Internal Network Range**: `_________________` (e.g., 192.168.1.0/24) +- **Public IP Address** (if applicable): `_________________` +- **Domain Name** (if applicable): `_________________` +- **Subdomain** (if applicable): `_________________` (e.g., punimtag.yourdomain.com) + +### 2.3 Firewall Rules +Please confirm that the following ports can be opened for the application: + +**Required Ports:** +- **Port 8000** (Backend API) - TCP + - [ ] Can be opened + - [ ] Cannot be opened (alternative port needed: `_________________`) +- **Port 3000** (Frontend) - TCP + - [ ] Can be opened + - [ ] Cannot be opened (alternative port needed: `_________________`) +- **Port 5432** (PostgreSQL) - TCP + - [ ] Can be opened (if database is on separate server) + - [ ] Internal only (localhost) + - [ ] Cannot be opened (alternative port needed: `_________________`) +- **Port 6379** (Redis) - TCP + - [ ] Can be opened (if Redis is on separate server) + - [ ] Internal only (localhost) + - [ ] Cannot be opened (alternative port needed: `_________________`) + +**Additional Ports (if using reverse proxy):** +- **Port 80** (HTTP) - TCP +- **Port 443** (HTTPS) - TCP + +### 2.4 Network Access Requirements +- **Who needs access to the application?** + - [ ] Internal users only (same network) + - [ ] External users (internet access) + - [ ] VPN users only + - [ ] Specific IP ranges: `_________________` + +- **Do users need to access from outside the network?** + - [ ] Yes (requires public IP or VPN) + - [ ] No (internal only) + +### 2.5 Proxy/VPN Configuration +- **Is there a proxy server?** + - [ ] Yes + - Proxy address: `_________________` + - Proxy port: `_________________` + - Authentication required: [ ] Yes [ ] No + - Credentials: `_________________` + - [ ] No + +- **VPN Requirements:** + - [ ] VPN access required for testing team + - [ ] VPN type: `_________________` (OpenVPN, Cisco AnyConnect, etc.) + - [ ] VPN credentials/configuration: `_________________` + +--- + +## 3. Database Configuration + +### 3.1 PostgreSQL Database +- **Database Server Location**: + - [ ] Same server as application + - [ ] Separate server (provide details below) + +**If separate database server:** +- **Database Server IP/Hostname**: `_________________` +- **Database Port**: `_________________` (default: 5432) +- **Database Name**: `_________________` (or we can create: `punimtag`) +- **Database Username**: `_________________` +- **Database Password**: `_________________` +- **Database Version**: `_________________` (PostgreSQL 12+ required) + +**If database needs to be created:** +- **Can we create the database?** [ ] Yes [ ] No +- **Database administrator credentials**: `_________________` +- **Preferred database name**: `_________________` + +### 3.2 Database Access +- **Network access to database**: + - [ ] Direct connection from application server + - [ ] VPN required + - [ ] Specific IP whitelist required: `_________________` + +### 3.3 Database Backup Requirements +- **Backup policy**: `_________________` +- **Backup location**: `_________________` +- **Backup schedule**: `_________________` + +### 3.4 Auth Database (Frontend Website Authentication) +The application uses a **separate authentication database** for the frontend website user accounts. + +- **Auth Database Server Location**: + - [ ] Same server as main database + - [ ] Same server as application (different database) + - [ ] Separate server (provide details below) + +**If separate auth database server:** +- **Auth Database Server IP/Hostname**: `_________________` +- **Auth Database Port**: `_________________` (default: 5432) +- **Auth Database Name**: `_________________` (or we can create: `punimtag_auth`) +- **Auth Database Username**: `_________________` +- **Auth Database Password**: `_________________` +- **Auth Database Version**: `_________________` (PostgreSQL 12+ required) + +**If auth database needs to be created:** +- **Can we create the auth database?** [ ] Yes [ ] No +- **Database administrator credentials**: `_________________` +- **Preferred database name**: `_________________` (default: `punimtag_auth`) + +**Auth Database Access:** +- **Network access to auth database**: + - [ ] Direct connection from application server + - [ ] VPN required + - [ ] Specific IP whitelist required: `_________________` + +**Note:** The auth database stores user accounts for the frontend website (separate from backend admin users). It requires its own connection string configured as `DATABASE_URL_AUTH`. + +--- + +## 4. Redis Configuration + +### 4.1 Redis Server +- **Redis Server Location**: + - [ ] Same server as application + - [ ] Separate server (provide details below) + - [ ] Not installed (we can install) + +**If separate Redis server:** +- **Redis Server IP/Hostname**: `_________________` +- **Redis Port**: `_________________` (default: 6379) +- **Redis Password** (if password-protected): `_________________` + +**If Redis needs to be installed:** +- **Can we install Redis?** [ ] Yes [ ] No +- **Preferred installation method**: + - [ ] Package manager (apt/yum) + - [ ] Docker container + - [ ] Manual compilation + +--- + +## 5. Storage & File System + +### 5.1 Photo Storage +- **Storage Location**: `_________________` (e.g., /var/punimtag/photos, /data/uploads) +- **Storage Capacity**: `_________________` GB +- **Storage Type**: + - [ ] Local disk + - [ ] Network attached storage (NAS) + - [ ] Cloud storage (specify: `_________________`) +- **Storage Path Permissions**: + - [ ] We can create and configure + - [ ] Pre-configured (provide path: `_________________`) + +### 5.2 File System Access +- **Mount points** (if using NAS): `_________________` +- **NFS/SMB configuration** (if applicable): `_________________` +- **Disk quotas**: `_________________` (if applicable) + +--- + +## 6. Software Prerequisites + +### 6.1 Installed Software +Please confirm if the following are already installed: + +**Backend Requirements:** +- **Python 3.12+**: + - [ ] Installed (version: `_________________`) + - [ ] Not installed (we can install) +- **PostgreSQL**: + - [ ] Installed (version: `_________________`) + - [ ] Not installed (we can install) +- **Redis**: + - [ ] Installed (version: `_________________`) + - [ ] Not installed (we can install) + +**Frontend Requirements:** +- **Node.js 18+**: + - [ ] Installed (version: `_________________`) + - [ ] Not installed (we can install) +- **npm**: + - [ ] Installed (version: `_________________`) + - [ ] Not installed (we can install) +- **Web Server** (for serving built frontend): + - [ ] Nginx (version: `_________________`) + - [ ] Apache (version: `_________________`) + - [ ] Other: `_________________` + - [ ] Not installed (we can install/configure) + +**Development Tools:** +- **Git**: + - [ ] Installed + - [ ] Not installed (we can install) + +### 6.2 Installation Permissions +- **Can we install software packages?** [ ] Yes [ ] No +- **Package manager available**: + - [ ] apt (Debian/Ubuntu) + - [ ] yum/dnf (RHEL/CentOS) + - [ ] Other: `_________________` + +### 6.3 Internet Access +- **Does the server have internet access?** [ ] Yes [ ] No +- **If yes, can it download packages?** [ ] Yes [ ] No +- **If no, do you have an internal package repository?** + - [ ] Yes (provide details: `_________________`) + - [ ] No + +--- + +## 7. Security & Authentication + +### 7.1 SSL/TLS Certificates +- **SSL Certificate Required?** + - [ ] Yes (HTTPS required) + - [ ] No (HTTP acceptable for testing) +- **Certificate Type**: + - [ ] Self-signed (we can generate) + - [ ] Organization CA certificate + - [ ] Let's Encrypt + - [ ] Commercial certificate +- **Certificate Location** (if provided): `_________________` + +### 7.2 Authentication & Access Control +- **Default Admin Credentials**: + - Username: `_________________` (or use default: `admin`) + - Password: `_________________` (or use default: `admin`) +- **User Accounts**: + - [ ] Single admin account only + - [ ] Multiple test user accounts needed + - Number of test users: `_________________` + - User details: `_________________` + +### 7.3 Security Policies +- **Firewall rules**: + - [ ] Managed by IT team (provide contact: `_________________`) + - [ ] We can configure +- **Security scanning requirements**: `_________________` +- **Compliance requirements**: `_________________` (e.g., HIPAA, GDPR, SOC 2) + +--- + +## 8. Monitoring & Logging + +### 8.1 Logging +- **Log file location**: `_________________` (default: application directory) +- **Log retention policy**: `_________________` +- **Centralized logging system**: + - [ ] Yes (provide details: `_________________`) + - [ ] No + +### 8.2 Monitoring +- **Monitoring tools in use**: `_________________` +- **Do you need application metrics?** [ ] Yes [ ] No +- **Health check endpoints**: + - [ ] Available at `/api/v1/health` + - [ ] Custom endpoint needed: `_________________` + +--- + +## 9. Testing Requirements + +### 9.1 Test Data +- **Sample photos for testing**: + - [ ] We will provide test photos + - [ ] You will provide test photos + - [ ] Location of test photos: `_________________` +- **Expected photo volume for testing**: `_________________` photos +- **Photo size range**: `_________________` MB per photo + +### 9.2 Test Users +- **Number of concurrent test users**: `_________________` +- **Test user accounts needed**: + - [ ] Yes (provide usernames: `_________________`) + - [ ] No (use default admin account) + +### 9.3 Testing Schedule +- **Preferred testing window**: + - Start date: `_________________` + - End date: `_________________` + - Preferred time: `_________________` (timezone: `_________________`) +- **Maintenance windows** (if any): `_________________` + +--- + +## 10. Frontend Website Configuration + +### 10.1 Frontend Deployment Method +- **How will the frontend be served?** + - [ ] Development mode (Vite dev server on port 3000) + - [ ] Production build served by web server (Nginx/Apache) + - [ ] Static file hosting (CDN, S3, etc.) + - [ ] Docker container + - [ ] Other: `_________________` + +### 10.2 Frontend Environment Variables +The frontend React application requires the following configuration: + +- **Backend API URL** (`VITE_API_URL`): + - Development: `http://localhost:8000` or `http://127.0.0.1:8000` + - Production: `_________________` (e.g., `https://api.yourdomain.com` or `http://server-ip:8000`) + - **Note:** This must be accessible from users' browsers (not just localhost) + +### 10.3 Frontend Build Requirements +- **Build location**: `_________________` (where built files will be placed) +- **Build process**: + - [ ] We will build on the server + - [ ] We will provide pre-built files + - [ ] Build will be done on a separate build server +- **Static file serving**: + - [ ] Nginx configured + - [ ] Apache configured + - [ ] Needs to be configured: `_________________` + +### 10.4 Frontend Access +- **Frontend URL/Domain**: `_________________` (e.g., `https://punimtag.yourdomain.com` or `http://server-ip:3000`) +- **HTTPS Required?** + - [ ] Yes (SSL certificate needed) + - [ ] No (HTTP acceptable for testing) +- **CORS Configuration**: + - [ ] Needs to be configured + - [ ] Already configured + - **Allowed origins**: `_________________` + +--- + +## 11. Deployment Method + +### 11.1 Preferred Deployment +- **Deployment method**: + - [ ] Direct installation on server + - [ ] Docker containers + - [ ] Docker Compose + - [ ] Kubernetes + - [ ] Other: `_________________` + +### 11.2 Code Deployment +- **How will code be deployed?** + - [ ] Git repository access (provide URL: `_________________`) + - [ ] File transfer (SFTP/SCP) + - [ ] We will provide deployment package +- **Repository access credentials**: `_________________` + +--- + +## 12. Environment Variables Summary + +For your reference, here are all the environment variables that need to be configured: + +**Backend Environment Variables:** +- `DATABASE_URL` - Main database connection (PostgreSQL or SQLite) + - Example: `postgresql+psycopg2://user:password@host:5432/punimtag` +- `DATABASE_URL_AUTH` - Auth database connection for frontend website users (PostgreSQL) + - Example: `postgresql+psycopg2://user:password@host:5432/punimtag_auth` +- `SECRET_KEY` - JWT secret key (change in production!) +- `ADMIN_USERNAME` - Default admin username (optional, for backward compatibility) +- `ADMIN_PASSWORD` - Default admin password (optional, for backward compatibility) +- `PHOTO_STORAGE_DIR` - Directory for storing uploaded photos (default: `data/uploads`) + +**Frontend Environment Variables:** +- `VITE_API_URL` - Backend API URL (must be accessible from browsers) + - Example: `http://server-ip:8000` or `https://api.yourdomain.com` + +**Note:** All environment variables should be set securely and not exposed in version control. + +--- + +## 13. Contact Information + +### 13.1 Primary Contacts +- **IT/Network Administrator**: + - Name: `_________________` + - Email: `_________________` + - Phone: `_________________` +- **Database Administrator**: + - Name: `_________________` + - Email: `_________________` + - Phone: `_________________` +- **Project Manager/Point of Contact**: + - Name: `_________________` + - Email: `_________________` + - Phone: `_________________` + +### 13.2 Emergency Contacts +- **After-hours support contact**: `_________________` +- **Escalation procedure**: `_________________` + +--- + +## 14. Additional Requirements + +### 14.1 Custom Configuration +- **Custom domain/subdomain**: `_________________` +- **Custom branding**: `_________________` +- **Integration requirements**: `_________________` +- **Special network requirements**: `_________________` + +### 14.2 Documentation +- **Network diagrams**: `_________________` (if available) +- **Existing infrastructure documentation**: `_________________` +- **Change management process**: `_________________` + +### 14.3 Other Notes +- **Any other relevant information**: + ``` + _________________________________________________ + _________________________________________________ + _________________________________________________ + ``` + +--- + +## Application Requirements Summary + +For your reference, here are the key technical requirements: + +**Application Components:** +- Backend API (FastAPI) - Port 8000 +- Frontend Website (React) - Port 3000 (dev) or served via web server (production) +- Main PostgreSQL Database - Port 5432 (stores photos, faces, people, tags) +- Auth PostgreSQL Database - Port 5432 (stores frontend website user accounts) +- Redis (for background jobs) - Port 6379 + +**System Requirements:** +- Python 3.12 or higher (backend) +- Node.js 18 or higher (frontend build) +- PostgreSQL 12 or higher (both databases) +- Redis 5.0 or higher +- Web server (Nginx/Apache) for production frontend serving +- Minimum 4GB RAM (8GB+ recommended) +- Sufficient disk space for photo storage + +**Network Requirements:** +- TCP ports: 3000 (dev frontend), 8000 (backend API) +- TCP ports: 5432 (databases), 6379 (Redis) - if services are remote +- HTTP/HTTPS access for users to frontend website +- Network connectivity between: + - Application server ↔ Main database + - Application server ↔ Auth database + - Application server ↔ Redis + - Users' browsers ↔ Frontend website + - Users' browsers ↔ Backend API (via VITE_API_URL) + +--- + +## Next Steps + +Once this information is provided, we will: +1. Review the network configuration +2. Prepare deployment scripts and configuration files +3. Schedule a deployment window +4. Perform initial setup and testing +5. Provide access credentials and documentation + +**Please return this completed form to:** `_________________` + +**Deadline for information:** `_________________` + +--- + +*Document Version: 1.0* +*Last Updated: [Current Date]* + diff --git a/docs/RESOURCE_REQUIREMENTS.md b/docs/RESOURCE_REQUIREMENTS.md new file mode 100644 index 0000000..27b9592 --- /dev/null +++ b/docs/RESOURCE_REQUIREMENTS.md @@ -0,0 +1,401 @@ +# PunimTag Resource Requirements + +**How the Software Works & What Resources You Need** + +This document explains how PunimTag works and what infrastructure resources you need to provision for Development, QA, and Production environments. + +--- + +## How the Software Works + +### System Architecture + +PunimTag consists of **5 main components** that work together: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ USER'S BROWSER │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Frontend Website (React) │ │ +│ │ - User interface for viewing/searching photos │ │ +│ │ - User authentication │ │ +│ │ - Photo uploads, face identification, tagging │ │ +│ └──────────────┬──────────────────────────────────────┘ │ +└──────────────────┼──────────────────────────────────────────┘ + │ HTTP/HTTPS + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ APPLICATION SERVER │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Backend API (FastAPI - Python) │ │ +│ │ - Handles all requests from frontend │ │ +│ │ - Manages photos, faces, people, tags │ │ +│ │ - Processes authentication │ │ +│ │ - Port: 8000 │ │ +│ └──────────────┬──────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────▼──────────────────────────────────────┐ │ +│ │ Background Worker (Python) │ │ +│ │ - Processes photos in background │ │ +│ │ - Detects faces using AI (DeepFace) │ │ +│ │ - Generates face encodings │ │ +│ │ - CPU-intensive work │ │ +│ └──────────────┬──────────────────────────────────────┘ │ +└──────────────────┼──────────────────────────────────────────┘ + │ + ┌──────────┼──────────┬──────────┬──────────┐ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Main │ │ Auth │ │ Redis │ │ Photo │ │ Web │ +│ Database │ │ Database │ │ Queue │ │ Storage │ │ Server │ +│(Postgres)│ │(Postgres)│ │ │ │ (Disk) │ │(Nginx) │ +│ │ │ │ │ │ │ │ │ │ +│ Photos │ │ Frontend │ │ Job │ │ Uploaded │ │ Serves │ +│ Faces │ │ Users │ │ Queue │ │ Photos │ │ Frontend│ +│ People │ │ Accounts │ │ │ │ │ │ Files │ +│ Tags │ │ │ │ │ │ │ │ │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ +``` + +### Component Responsibilities + +1. **Frontend Website (React)** + - What it does: User interface that runs in browsers + - Users interact with: View photos, search, identify faces, upload photos, tag photos + - Technology: React + TypeScript, built with Vite + - Served by: Web server (Nginx/Apache) in production, or dev server for testing + +2. **Backend API (FastAPI)** + - What it does: Handles all business logic, processes requests from frontend + - Responsibilities: + - Authentication and authorization + - Photo management (upload, scan, delete) + - Face detection and processing (queues jobs) + - Person identification and management + - Tag management + - Search functionality + - Technology: Python 3.12+, FastAPI framework + - Port: 8000 (configurable) + +3. **Background Worker** + - What it does: Processes heavy tasks in the background + - Responsibilities: + - Face detection in photos (uses AI models) + - Generating face encodings (512-dimensional vectors) + - Processing photos uploaded by users + - Technology: Python 3.12+, uses DeepFace AI library + - Runs: As a separate process, connects to Redis for job queue + +4. **Main Database (PostgreSQL)** + - What it stores: + - Photos metadata (paths, dates, file info) + - Detected faces (locations, encodings, quality scores) + - People records (names, dates of birth) + - Tags and photo-tag relationships + - Technology: PostgreSQL 12+ + - Size: Grows with number of photos and faces + +5. **Auth Database (PostgreSQL)** + - What it stores: + - Frontend website user accounts + - User-uploaded photos (pending approval) + - User face identifications (pending approval) + - User tag suggestions (pending approval) + - Reported photos + - Technology: PostgreSQL 12+ + - Size: Small, grows with number of users + +6. **Redis** + - What it does: Job queue for background processing + - Stores: Background job status and progress + - Technology: Redis 5.0+ + - Size: Very small (just job metadata) + +7. **Photo Storage (File System)** + - What it stores: Actual photo and video files + - Location: Directory on server or network storage + - Size: Depends on number and size of photos/videos + +8. **Web Server (Nginx/Apache)** + - What it does: Serves the frontend website files in production + - Technology: Nginx or Apache + - Ports: 80 (HTTP), 443 (HTTPS) + +--- + +## Resource Requirements by Environment + +### Development Environment + +**Purpose:** For development, testing new features, debugging + +**Components Needed:** +- 1 Application Server (can run all services) +- 1 Database Server (can host both databases) +- 1 Redis instance (can be on application server) + +**Server Specifications:** +- **CPU:** 2-4 cores (sufficient for development) +- **RAM:** 4-8 GB (enough for development workloads) +- **Storage:** + - OS + Software: 20 GB + - Database: 10-50 GB (depends on test data) + - Photo storage: 50-200 GB (test photos) + - **Total: 80-270 GB** + +**Network:** +- Port 8000 (Backend API) +- Port 3000 (Frontend dev server) OR port 80/443 (if using web server) +- Internal network access sufficient + +**Software:** +- Python 3.12+ +- Node.js 18+ (for building frontend) +- PostgreSQL 12+ +- Redis 5.0+ +- Git + +**Notes:** +- Can run everything on a single server +- Lower performance requirements +- May use SQLite for databases (not recommended but possible) +- Development mode frontend (Vite dev server) is fine + +--- + +### QA Environment + +**Purpose:** For quality assurance testing, user acceptance testing, staging + +**Components Needed:** +- 1 Application Server +- 1 Database Server (recommended separate, but can be same as app server) +- 1 Redis instance (can be on application server) + +**Server Specifications:** +- **CPU:** 4-8 cores (should handle moderate load) +- **RAM:** 8-16 GB (needs to handle concurrent users and processing) +- **Storage:** + - OS + Software: 20 GB + - Database: 50-200 GB (larger test dataset) + - Photo storage: 200 GB - 1 TB (realistic photo collection) + - **Total: 270 GB - 1.2 TB** + +**Network:** +- Port 8000 (Backend API) +- Port 80/443 (Frontend via web server - production-like) +- Should be accessible to QA team (internal network or VPN) + +**Software:** +- Python 3.12+ +- Node.js 18+ (for building frontend) +- PostgreSQL 12+ +- Redis 5.0+ +- Nginx or Apache (for serving frontend) +- Git + +**Notes:** +- Should mirror production setup as closely as possible +- Use production build of frontend (not dev server) +- Should have realistic data volumes for testing +- Performance should be similar to production + +--- + +### Production Environment + +**Purpose:** Live system for end users + +**Components Needed:** +- 1-2 Application Servers (can scale horizontally) +- 1 Database Server (dedicated, high performance) +- 1 Redis Server (can be on database server or separate) +- 1 Web Server (for serving frontend, can be on app server or separate) +- Optional: Load balancer (if multiple app servers) + +**Server Specifications:** + +**Application Server(s):** +- **CPU:** 8-16 cores (more cores = faster face processing) +- **RAM:** 16-32 GB (handles concurrent users and background processing) +- **Storage:** + - OS + Software: 50 GB + - Application code: 5 GB + - Logs: 10-50 GB (with rotation) + - **Total: 65-105 GB per server** + +**Database Server:** +- **CPU:** 8-16 cores (database queries and indexing) +- **RAM:** 16-64 GB (depends on database size, more RAM = better performance) +- **Storage:** + - OS + Software: 50 GB + - Database data: 100 GB - 5 TB+ (depends on photo collection size) + - Database backups: 100 GB - 5 TB+ (same as data) + - **Total: 250 GB - 10 TB+** +- **Storage Type:** SSD recommended for better performance +- **Backup:** Automated daily backups recommended + +**Photo Storage:** +- **Storage:** 500 GB - 50 TB+ (depends on photo collection) +- **Location:** Can be on database server, separate storage server, or network storage (NAS) +- **Type:** Network storage (NAS) recommended for large collections + +**Redis Server:** +- **CPU:** 2-4 cores (lightweight) +- **RAM:** 2-4 GB (very small data) +- **Storage:** 10 GB (minimal) +- Can run on database server or application server + +**Web Server (if separate):** +- **CPU:** 2-4 cores +- **RAM:** 2-4 GB +- **Storage:** 10 GB +- Can run on application server + +**Network:** +- Port 8000 (Backend API) - internal or load balancer +- Port 80/443 (Frontend via web server) - public access +- Port 5432 (Database) - internal only +- Port 6379 (Redis) - internal only +- HTTPS/SSL certificate required + +**Software:** +- Python 3.12+ +- Node.js 18+ (for building frontend) +- PostgreSQL 12+ (latest stable recommended) +- Redis 5.0+ +- Nginx or Apache (for serving frontend) +- SSL certificate (for HTTPS) +- Monitoring tools (optional but recommended) + +**High Availability (Optional):** +- Multiple application servers behind load balancer +- Database replication (master-slave) +- Redis replication +- Automated backups +- Monitoring and alerting + +--- + +## Resource Sizing Guidelines + +### Database Size Estimation + +**Main Database:** +- Per photo: ~1-5 KB (metadata only, photos stored on disk) +- Per face: ~2-10 KB (encoding + metadata) +- Per person: ~1 KB +- Per tag: ~1 KB + +**Example:** +- 10,000 photos with 50,000 faces = ~250-500 MB +- 1,000 people = ~1 MB +- 100 tags = ~100 KB +- **Total: ~250-500 MB for 10K photos** + +**Auth Database:** +- Per user: ~1 KB +- Per pending photo: ~1 KB +- **Total: Usually < 100 MB** + +### Photo Storage Size Estimation + +- Average photo: 2-5 MB +- Average video: 50-200 MB +- 10,000 photos = 20-50 GB +- 100,000 photos = 200-500 GB +- 1,000,000 photos = 2-5 TB + +### CPU Requirements + +**Face Processing:** +- 1 photo with faces: 2-10 seconds (depends on number of faces) +- CPU-intensive: More cores = faster processing +- Background worker uses CPU heavily during processing + +**API Server:** +- Light CPU usage for most requests +- Higher CPU during photo uploads and searches + +### RAM Requirements + +**Application Server:** +- Base: 2-4 GB (OS + Python + FastAPI) +- Per concurrent user: ~50-100 MB +- Background worker: 2-4 GB (AI models loaded in memory) +- **Recommended: 16-32 GB for production** + +**Database Server:** +- Base: 2-4 GB (OS + PostgreSQL) +- Database cache: 25-50% of database size (PostgreSQL uses RAM for caching) +- **Recommended: 16-64 GB depending on database size** + +--- + +## Deployment Scenarios + +### Scenario 1: Small Deployment (Single Server) +**For:** Small organizations, < 10,000 photos, < 50 users + +- **1 Server:** Application + Database + Redis + Web Server +- **CPU:** 8 cores +- **RAM:** 16 GB +- **Storage:** 500 GB - 1 TB +- **Cost:** Low + +### Scenario 2: Medium Deployment (Separate Database) +**For:** Medium organizations, 10,000-100,000 photos, 50-200 users + +- **1 Application Server:** App + Worker + Web Server +- **1 Database Server:** Both databases +- **CPU:** 8-16 cores each +- **RAM:** 16-32 GB each +- **Storage:** 1-5 TB (database), 1-5 TB (photos) +- **Cost:** Medium + +### Scenario 3: Large Deployment (Fully Separated) +**For:** Large organizations, 100,000+ photos, 200+ users + +- **2+ Application Servers:** Behind load balancer +- **1 Database Server:** Main database (high performance) +- **1 Database Server:** Auth database (can be smaller) +- **1 Redis Server:** Job queue +- **1 Web Server:** Frontend (or CDN) +- **Storage Server/NAS:** Photo storage +- **CPU:** 16+ cores per server +- **RAM:** 32-64 GB per server +- **Storage:** 5-50+ TB +- **Cost:** High + +--- + +## Summary Checklist + +### Development Environment +- [ ] 1 server (4-8 GB RAM, 2-4 cores, 100-300 GB storage) +- [ ] PostgreSQL (both databases can be on same server) +- [ ] Redis +- [ ] Python 3.12+, Node.js 18+ + +### QA Environment +- [ ] 1-2 servers (8-16 GB RAM, 4-8 cores, 300 GB - 1 TB storage) +- [ ] PostgreSQL (separate server recommended) +- [ ] Redis +- [ ] Web server (Nginx/Apache) +- [ ] Python 3.12+, Node.js 18+ + +### Production Environment +- [ ] 1-2 application servers (16-32 GB RAM, 8-16 cores, 100 GB storage) +- [ ] 1 database server (16-64 GB RAM, 8-16 cores, 500 GB - 10 TB storage) +- [ ] Redis server (2-4 GB RAM, can be on database server) +- [ ] Web server (2-4 GB RAM, can be on app server) +- [ ] Photo storage (500 GB - 50 TB, can be on database server or separate) +- [ ] SSL certificate +- [ ] Backup solution +- [ ] Monitoring (optional but recommended) + +--- + +**Questions?** Contact us to discuss your specific requirements and we can help you size the infrastructure appropriately. + diff --git a/docs/TAG_PHOTOS_PERFORMANCE_ANALYSIS.md b/docs/TAG_PHOTOS_PERFORMANCE_ANALYSIS.md new file mode 100644 index 0000000..e7f36cd --- /dev/null +++ b/docs/TAG_PHOTOS_PERFORMANCE_ANALYSIS.md @@ -0,0 +1,234 @@ +# Tag Photos Performance Analysis + +## Executive Summary + +The Tag Photos page has significant performance bottlenecks, primarily in the backend database queries. The current implementation uses an N+1 query pattern that results in thousands of database queries for large photo collections. + +## Current Performance Issues + +### 1. Backend: N+1 Query Problem (CRITICAL) + +**Location:** `src/web/services/tag_service.py::get_photos_with_tags()` + +**Problem:** +- Loads all photos in one query (line 238-242) +- Then makes **4 separate queries per photo** in a loop: + 1. Face count query (line 247-251) + 2. Unidentified face count query (line 254-259) + 3. Tags query (line 262-269) + 4. People names query (line 272-280) + +**Impact:** +- For 1,000 photos: **1 + (1,000 × 4) = 4,001 database queries** +- For 10,000 photos: **1 + (10,000 × 4) = 40,001 database queries** +- Each query has network latency and database processing time +- This is the primary cause of slow loading + +**Example Timeline (estimated for 1,000 photos):** +- Initial photo query: ~50ms +- 1,000 face count queries: ~2,000ms (2ms each) +- 1,000 unidentified face count queries: ~2,000ms +- 1,000 tags queries: ~3,000ms (3ms each, includes joins) +- 1,000 people names queries: ~3,000ms (3ms each, includes joins) +- **Total: ~10+ seconds** (depending on database performance) + +### 2. Backend: Missing Database Indexes + +**Potential Issues:** +- `Face.photo_id` may not be indexed (affects face count queries) +- `PhotoTagLinkage.photo_id` may not be indexed (affects tag queries) +- `Face.person_id` may not be indexed (affects people names queries) +- Composite indexes may be missing for common query patterns + +### 3. Frontend: Loading All Data at Once + +**Location:** `frontend/src/pages/Tags.tsx::loadData()` + +**Problem:** +- Loads ALL photos and tags in a single request (line 103-106) +- No pagination or lazy loading +- For large collections, this means: + - Large JSON payload (network transfer time) + - Large JavaScript object in memory + - Slow initial render + +**Impact:** +- Network transfer time for large datasets +- Browser memory usage +- Initial render blocking + +### 4. Frontend: Expensive Computations on Every Render + +**Location:** `frontend/src/pages/Tags.tsx::folderGroups` (line 134-256) + +**Problem:** +- Complex `useMemo` that: + - Filters photos + - Groups by folder + - Sorts folders + - Sorts photos within folders +- Runs on every state change (photos, sortColumn, sortDir, showOnlyUnidentified) +- For large datasets, this can take 100-500ms + +**Impact:** +- UI freezes during computation +- Poor user experience when changing filters/sorting + +### 5. Frontend: Dialog Loading Performance + +**Location:** `frontend/src/pages/Tags.tsx::TagSelectedPhotosDialog` (line 1781-1799) + +**Problem:** +- When opening "Tag Selected Photos" dialog, loads tags for ALL selected photos sequentially +- Uses a `for` loop with await (line 1784-1792) +- No batching or parallelization + +**Impact:** +- If 100 photos selected: 100 sequential API calls +- Each call takes ~50-100ms +- **Total: 5-10 seconds** just to open the dialog + +### 6. Frontend: Unnecessary Re-renders + +**Problem:** +- Multiple `useEffect` hooks that trigger re-renders +- Folder state changes trigger full re-computation +- Dialog open/close triggers full data reload (line 1017-1020, 1038-1041, 1108-1112) + +**Impact:** +- Unnecessary API calls +- Unnecessary computations +- Poor perceived performance + +## Optimization Recommendations + +### Priority 1: Fix Backend N+1 Query Problem (HIGHEST IMPACT) + +**Solution: Use JOINs and Aggregations** + +Replace the loop-based approach with a single optimized query using: +- LEFT JOINs for related data +- GROUP BY with aggregations +- Subqueries or window functions for counts + +**Expected Improvement:** +- From 4,001 queries → **1-3 queries** +- From 10+ seconds → **< 1 second** (for 1,000 photos) + +**Implementation Approach:** +```python +# Use SQLAlchemy to build a single query with: +# - LEFT JOIN for faces (with COUNT aggregation) +# - LEFT JOIN for tags (with GROUP_CONCAT equivalent) +# - LEFT JOIN for people (with GROUP_CONCAT equivalent) +# - Subquery for unidentified face count +``` + +### Priority 2: Add Database Indexes + +**Required Indexes:** +- `faces.photo_id` (if not exists) +- `faces.person_id` (if not exists) +- `phototaglinkage.photo_id` (if not exists) +- `phototaglinkage.tag_id` (if not exists) +- Composite index: `(photo_id, person_id)` on faces table + +**Expected Improvement:** +- 20-50% faster query execution +- Better scalability + +### Priority 3: Implement Pagination + +**Backend:** +- Add `page` and `page_size` parameters to `get_photos_with_tags_endpoint` +- Return paginated results + +**Frontend:** +- Load photos in pages (e.g., 100 at a time) +- Implement infinite scroll or "Load More" button +- Only render visible photos (virtual scrolling) + +**Expected Improvement:** +- Initial load: **< 1 second** (first page only) +- Better perceived performance +- Lower memory usage + +### Priority 4: Optimize Frontend Computations + +**Solutions:** +1. **Memoization:** Better use of `useMemo` and `useCallback` +2. **Virtual Scrolling:** Only render visible rows (react-window or similar) +3. **Debouncing:** Debounce filter/sort changes +4. **Lazy Loading:** Load folder contents on expand + +**Expected Improvement:** +- Smooth UI interactions +- No freezing during filter/sort changes + +### Priority 5: Batch API Calls in Dialogs + +**Solution:** +- Create a batch endpoint: `GET /api/v1/tags/photos/batch?photo_ids=1,2,3...` +- Load tags for multiple photos in one request +- Or use Promise.all() for parallel requests (with limit) + +**Expected Improvement:** +- Dialog open time: From 5-10 seconds → **< 1 second** + +### Priority 6: Cache and State Management + +**Solutions:** +1. **SessionStorage:** Cache loaded photos (already partially done) +2. **Optimistic Updates:** Update UI immediately, sync in background +3. **Incremental Loading:** Load only changed data after mutations + +**Expected Improvement:** +- Faster subsequent loads +- Better user experience + +## Performance Metrics (Current vs. Optimized) + +### Current Performance (1,000 photos) +- **Initial Load:** 10-15 seconds +- **Filter/Sort Change:** 500ms-1s (UI freeze) +- **Dialog Open (100 photos):** 5-10 seconds +- **Database Queries:** 4,001 queries +- **Memory Usage:** High (all photos in memory) + +### Optimized Performance (1,000 photos) +- **Initial Load:** < 1 second (first page) +- **Filter/Sort Change:** < 100ms (smooth) +- **Dialog Open (100 photos):** < 1 second +- **Database Queries:** 1-3 queries +- **Memory Usage:** Low (only visible photos) + +## Implementation Priority + +1. **Phase 1 (Critical):** ✅ Fix backend N+1 queries - **COMPLETED** + - Rewrote `get_photos_with_tags()` to use 3 queries instead of 4N+1 + - Query 1: Photos with face counts (LEFT JOIN + GROUP BY) + - Query 2: All tags for all photos (single query with IN clause) + - Query 3: All people for all photos (single query with IN clause) + - Expected improvement: 10+ seconds → < 1 second for 1,000 photos + +2. **Phase 2 (High):** Add database indexes +3. **Phase 3 (High):** Implement pagination +4. **Phase 4 (Medium):** Optimize frontend computations +5. **Phase 5 (Medium):** Batch API calls in dialogs +6. **Phase 6 (Low):** Advanced caching and state management + +## Testing Recommendations + +1. **Load Testing:** Test with 1,000, 5,000, and 10,000 photos +2. **Database Profiling:** Use query profiling to identify slow queries +3. **Frontend Profiling:** Use React DevTools Profiler +4. **Network Analysis:** Monitor API response times +5. **User Testing:** Measure perceived performance + +## Additional Considerations + +1. **Progressive Loading:** Show skeleton screens while loading +2. **Error Handling:** Graceful degradation if queries fail +3. **Monitoring:** Add performance metrics/logging +4. **Documentation:** Document query patterns and indexes + diff --git a/docs/VIDEO_PERSON_IDENTIFICATION_ANALYSIS.md b/docs/VIDEO_PERSON_IDENTIFICATION_ANALYSIS.md new file mode 100644 index 0000000..9af6e02 --- /dev/null +++ b/docs/VIDEO_PERSON_IDENTIFICATION_ANALYSIS.md @@ -0,0 +1,642 @@ +# Analysis: Identifying People in Videos + +**Date:** December 2024 +**Status:** Analysis Only (No Implementation) +**Feature:** Direct person identification in videos without face detection + +--- + +## Executive Summary + +This document analyzes how to implement the ability to identify people directly in videos within the "Identify People" tab. Unlike photos where people are identified through detected faces, videos will allow direct person-to-video associations without requiring face detection or frame extraction. + +**Key Requirements:** +- List videos with filtering capabilities +- Each video can have multiple people identified +- Can add more people to a video even after some are already identified +- Located in "Identify People" tab under "Identify People in Videos" sub-tab + +--- + +## Current System Architecture + +### Database Schema + +**Current Relationships:** +- `Photo` (includes videos via `media_type='video'`) → `Face` → `Person` +- People are linked to photos **only** through faces +- No direct Photo-Person relationship exists + +**Relevant Models:** +```python +class Photo: + id: int + path: str + media_type: str # "image" or "video" + # ... other fields + +class Face: + id: int + photo_id: int # FK to Photo + person_id: int # FK to Person (nullable) + # ... encoding, location, etc. + +class Person: + id: int + first_name: str + last_name: str + # ... other fields +``` + +**Current State:** +- Videos are stored in `photos` table with `media_type='video'` +- Videos are marked as `processed=True` but face processing is skipped +- No faces exist for videos currently +- People cannot be linked to videos through the existing Face model + +### Frontend Structure + +**Identify Page (`frontend/src/pages/Identify.tsx`):** +- Has two tabs: "Identify Faces" and "Identify People in Videos" +- Videos tab currently shows placeholder: "This functionality will be available in a future update" +- Faces tab has full functionality for identifying people through faces + +### API Endpoints + +**Existing Photo/Video Endpoints:** +- `GET /api/v1/photos` - Search photos/videos with `media_type` filter +- Supports filtering by `media_type='video'` to get videos +- Returns `PhotoSearchResult` with video metadata + +**No Existing Endpoints For:** +- Listing videos specifically for person identification +- Getting people associated with a video +- Identifying people in videos +- Managing video-person relationships + +--- + +## Proposed Solution + +### 1. Database Schema Changes + +#### Option A: New PhotoPersonLinkage Table (Recommended) + +Create a new junction table to link people directly to photos/videos: + +```python +class PhotoPersonLinkage(Base): + """Direct linkage between Photo/Video and Person (without faces).""" + + __tablename__ = "photo_person_linkage" + + id = Column(Integer, primary_key=True, autoincrement=True) + photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True) + person_id = Column(Integer, ForeignKey("people.id"), nullable=False, index=True) + identified_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) + created_date = Column(DateTime, default=datetime.utcnow, nullable=False) + + photo = relationship("Photo", back_populates="direct_people") + person = relationship("Person", back_populates="direct_photos") + + __table_args__ = ( + UniqueConstraint("photo_id", "person_id", name="uq_photo_person"), + Index("idx_photo_person_photo", "photo_id"), + Index("idx_photo_person_person", "person_id"), + ) +``` + +**Update Photo Model:** +```python +class Photo(Base): + # ... existing fields ... + direct_people = relationship("PhotoPersonLinkage", back_populates="photo", cascade="all, delete-orphan") +``` + +**Update Person Model:** +```python +class Person(Base): + # ... existing fields ... + direct_photos = relationship("PhotoPersonLinkage", back_populates="person", cascade="all, delete-orphan") +``` + +**Pros:** +- ✅ Clean separation: Face-based identification vs. direct identification +- ✅ Supports both photos and videos (unified approach) +- ✅ Can track who identified the person (`identified_by_user_id`) +- ✅ Prevents duplicate person-video associations +- ✅ Similar pattern to `PhotoTagLinkage` (consistent architecture) + +**Cons:** +- ⚠️ Requires database migration +- ⚠️ Need to update queries that get "people in photo" to check both Face and PhotoPersonLinkage + +#### Option B: Use Face Model with Dummy Faces (Not Recommended) + +Create "virtual" faces for videos without encodings. + +**Cons:** +- ❌ Misleading data model (faces without actual face data) +- ❌ Breaks assumptions about Face model (encoding required) +- ❌ Confusing for queries and logic +- ❌ Not semantically correct + +**Recommendation:** Option A (PhotoPersonLinkage table) + +### 2. API Endpoints + +#### 2.1 List Videos for Identification + +**Endpoint:** `GET /api/v1/videos` + +**Query Parameters:** +- `page`: int (default: 1) +- `page_size`: int (default: 50, max: 200) +- `folder_path`: Optional[str] - Filter by folder +- `date_from`: Optional[str] - Filter by date taken (from) +- `date_to`: Optional[str] - Filter by date taken (to) +- `has_people`: Optional[bool] - Filter videos with/without identified people +- `person_name`: Optional[str] - Filter videos containing specific person +- `sort_by`: str (default: "filename") - "filename", "date_taken", "date_added" +- `sort_dir`: str (default: "asc") - "asc" or "desc" + +**Response:** +```python +class VideoListItem(BaseModel): + id: int + filename: str + path: str + date_taken: Optional[date] + date_added: date + identified_people: List[PersonInfo] # People identified in this video + identified_people_count: int + +class ListVideosResponse(BaseModel): + items: List[VideoListItem] + page: int + page_size: int + total: int +``` + +#### 2.2 Get People in a Video + +**Endpoint:** `GET /api/v1/videos/{video_id}/people` + +**Response:** +```python +class VideoPersonInfo(BaseModel): + person_id: int + first_name: str + last_name: str + middle_name: Optional[str] + maiden_name: Optional[str] + date_of_birth: Optional[date] + identified_by: Optional[str] # Username + identified_date: datetime + +class VideoPeopleResponse(BaseModel): + video_id: int + people: List[VideoPersonInfo] +``` + +#### 2.3 Identify People in Video + +**Endpoint:** `POST /api/v1/videos/{video_id}/identify` + +**Request:** +```python +class IdentifyVideoRequest(BaseModel): + person_id: Optional[int] = None # Use existing person + first_name: Optional[str] = None # Create new person + last_name: Optional[str] = None + middle_name: Optional[str] = None + maiden_name: Optional[str] = None + date_of_birth: Optional[date] = None +``` + +**Response:** +```python +class IdentifyVideoResponse(BaseModel): + video_id: int + person_id: int + created_person: bool + message: str +``` + +**Behavior:** +- If `person_id` provided: Link existing person to video +- If person info provided: Create new person and link to video +- If person already linked: Return success (idempotent) +- Track `identified_by_user_id` for audit + +#### 2.4 Remove Person from Video + +**Endpoint:** `DELETE /api/v1/videos/{video_id}/people/{person_id}` + +**Response:** +```python +class RemoveVideoPersonResponse(BaseModel): + video_id: int + person_id: int + removed: bool + message: str +``` + +### 3. Service Layer Functions + +#### 3.1 Video Service Functions + +**File:** `src/web/services/video_service.py` (new file) + +```python +def list_videos_for_identification( + db: Session, + folder_path: Optional[str] = None, + date_from: Optional[date] = None, + date_to: Optional[date] = None, + has_people: Optional[bool] = None, + person_name: Optional[str] = None, + sort_by: str = "filename", + sort_dir: str = "asc", + page: int = 1, + page_size: int = 50, +) -> Tuple[List[Photo], int]: + """List videos for person identification.""" + # Query videos (media_type='video') + # Apply filters + # Join with PhotoPersonLinkage to get people count + # Return paginated results + +def get_video_people( + db: Session, + video_id: int, +) -> List[Tuple[Person, PhotoPersonLinkage]]: + """Get all people identified in a video.""" + # Query PhotoPersonLinkage for video_id + # Join with Person + # Return list with identification metadata + +def identify_person_in_video( + db: Session, + video_id: int, + person_id: Optional[int] = None, + person_data: Optional[dict] = None, + user_id: Optional[int] = None, +) -> Tuple[Person, bool]: + """Identify a person in a video. + + Returns: + (Person, created_person: bool) + """ + # Validate video exists and is actually a video + # Get or create person + # Create PhotoPersonLinkage if doesn't exist + # Return person and created flag + +def remove_person_from_video( + db: Session, + video_id: int, + person_id: int, +) -> bool: + """Remove person identification from video.""" + # Delete PhotoPersonLinkage + # Return success +``` + +#### 3.2 Update Existing Search Functions + +**File:** `src/web/services/search_service.py` + +Update `get_photo_person()` to check both: +1. Face-based identification (existing) +2. Direct PhotoPersonLinkage (new) + +```python +def get_photo_people(db: Session, photo_id: int) -> List[Person]: + """Get all people in a photo/video (both face-based and direct).""" + people = [] + + # Get people through faces + face_people = ( + db.query(Person) + .join(Face, Person.id == Face.person_id) + .filter(Face.photo_id == photo_id) + .distinct() + .all() + ) + people.extend(face_people) + + # Get people through direct linkage + direct_people = ( + db.query(Person) + .join(PhotoPersonLinkage, Person.id == PhotoPersonLinkage.person_id) + .filter(PhotoPersonLinkage.photo_id == photo_id) + .distinct() + .all() + ) + people.extend(direct_people) + + # Remove duplicates + seen_ids = set() + unique_people = [] + for person in people: + if person.id not in seen_ids: + seen_ids.add(person.id) + unique_people.append(person) + + return unique_people +``` + +### 4. Frontend Implementation + +#### 4.1 Video List Component + +**Location:** `frontend/src/pages/Identify.tsx` (videos tab) + +**Features:** +- Video grid/list view with thumbnails +- Filter panel: + - Folder path + - Date range (date taken) + - Has people / No people + - Person name search +- Sort options: filename, date taken, date added +- Pagination +- Each video shows: + - Thumbnail (video poster/first frame) + - Filename + - Date taken + - List of identified people (badges/chips) + - "Identify People" button + +#### 4.2 Video Detail / Identification Panel + +**When video is selected:** + +**Left Panel:** +- Video player (HTML5 `