Skip to content

Fixed face count inconsistency after folder removal #573

Open
DecodeX15 wants to merge 30 commits into
AOSSIE-Org:mainfrom
DecodeX15:fix/face_inconsistency_after_folder_removal
Open

Fixed face count inconsistency after folder removal #573
DecodeX15 wants to merge 30 commits into
AOSSIE-Org:mainfrom
DecodeX15:fix/face_inconsistency_after_folder_removal

Conversation

@DecodeX15
Copy link
Copy Markdown
Member

@DecodeX15 DecodeX15 commented Oct 7, 2025

Title:
fix(#569): remove clusters with zero face_count from persisted face data

Description:
This PR filters out clusters where face_count === 0 before updating the main array.
By excluding these empty clusters, face data no longer persists after the source folder is removed.

Uploading Recording 2025-10-07 095004.mp4…

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced multi-service startup orchestration with automatic service health checks.
    • Added automatic cleanup of empty clusters during folder operations.
  • Improvements

    • Optimized cache invalidation strategy for better performance.
    • Refined API schema definitions for improved compatibility.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 7, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Backend adds cluster cleanup after folder operations, app initializes cache on startup with selective refresh, frontend rewrites service spawning with health checks, and OpenAPI schemas are refined while a MediaView folder action is disabled.

Changes

System Cleanup and Infrastructure Updates

Layer / File(s) Summary
Cluster Cleanup Integration
backend/app/routes/folders.py
After folder add, sync, and delete operations complete, routes now invoke cluster_util_delete_empty_clusters() to remove orphaned cluster entries with no associated faces. Import section reorganized to include cluster utilities and executor.
Cache Management on Startup and Refresh
app.py
New initialize_app() startup hook calls invalidate_cache() with no arguments to clear entire cache. Existing refresh_data() updated to call invalidate_cache() with two specific keys (albums:get_all_albums, folder_structure:get_folder_structure).
Multi-Service Startup Orchestration
frontend/src/utils/serverUtils.ts
startServer() rewritten to resolve resources folder, conditionally spawn backend and sync microservice via platform-specific commands (Windows vs Unix), attach stderr listeners to spawned processes, log PIDs, and probe /health endpoints via isServerRunning() and isSyncServiceRunning() helpers with error handling.
API Schema and UI Component Adjustments
docs/backend/backend_python/openapi.json, frontend/src/components/Media/MediaView.tsx
OpenAPI: input_type query parameter schema simplified from allOf wrapper to direct $ref to InputType; ImageInCluster.metadata now allows additionalProperties: true. MediaView: handleOpenFolder implementation commented out, disabling Tauri revealItemInDir call.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AOSSIE-Org/PictoPy#577: Directly opposes this PR's disabling of handleOpenFolder folder reveal action by having added the feature in the first place.
  • AOSSIE-Org/PictoPy#524: Introduced the /face-clusters/face-search endpoint whose OpenAPI schema is refined by this PR's updates to input_type and ImageInCluster.metadata.
  • AOSSIE-Org/PictoPy#976: Re-enables the MediaView folder opening functionality that this PR disables by commenting out handleOpenFolder.

Suggested labels

Python, TypeScript/JavaScript, Documentation

Suggested reviewers

  • rahulharpal1603
  • Aditya30ag

🐰 In clusters left alone with no embrace,
We sweep them clean with gentle grace,
Services spark to life anew,
While caches reset through and through,
And folders rest in quiet place!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR—fixing face count inconsistency that occurs after folder removal—which aligns with the core changes across multiple files to delete empty clusters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20d16e5 and 24dba76.

📒 Files selected for processing (1)
  • frontend/src/components/FaceCollections.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/FaceCollections.tsx (1)
frontend/src/types/Media.ts (1)
  • Cluster (42-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Backend Tests
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)

Comment on lines +23 to +32
useEffect(() => {
if (clustersSuccess && clustersData?.data?.clusters) {
const clusters = (clustersData.data.clusters || []) as Cluster[];
dispatch(setClusters(clusters));
}
}, [clustersData, clustersSuccess, dispatch]);
if (clustersSuccess && clustersData?.data?.clusters) {
const clusters = (clustersData.data.clusters || []) as Cluster[];

const filteredClusters = clusters.filter(c => c.face_count > 0);

dispatch(setClusters(filteredClusters));
console.log(filteredClusters);
}
}, [clustersData, clustersSuccess, dispatch]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Filtering logic is correct; remove console.log and improve variable naming.

The core fix correctly filters out clusters with face_count === 0, which addresses the stated objective of preventing face data persistence after folder removal.

However:

  1. Remove the console.log on line 30 before merging to production. Logging filtered data is helpful during development but should not ship to users.

  2. The local variable clusters on line 25 shadows the Redux state variable from line 16, making the code harder to follow. Consider renaming the local variable to allClusters or fetchedClusters.

  3. The || [] fallback on line 25 is redundant since line 24 already ensures clustersData?.data?.clusters exists via optional chaining.

Apply this diff to address the issues:

  useEffect(() => {
   if (clustersSuccess && clustersData?.data?.clusters) {
-    const clusters = (clustersData.data.clusters || []) as Cluster[];
+    const fetchedClusters = clustersData.data.clusters as Cluster[];

-    const filteredClusters = clusters.filter(c => c.face_count > 0);
+    const filteredClusters = fetchedClusters.filter(c => c.face_count > 0);

     dispatch(setClusters(filteredClusters));
-    console.log(filteredClusters);
   }
 }, [clustersData, clustersSuccess, dispatch]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (clustersSuccess && clustersData?.data?.clusters) {
const clusters = (clustersData.data.clusters || []) as Cluster[];
dispatch(setClusters(clusters));
}
}, [clustersData, clustersSuccess, dispatch]);
if (clustersSuccess && clustersData?.data?.clusters) {
const clusters = (clustersData.data.clusters || []) as Cluster[];
const filteredClusters = clusters.filter(c => c.face_count > 0);
dispatch(setClusters(filteredClusters));
console.log(filteredClusters);
}
}, [clustersData, clustersSuccess, dispatch]);
useEffect(() => {
if (clustersSuccess && clustersData?.data?.clusters) {
const fetchedClusters = clustersData.data.clusters as Cluster[];
const filteredClusters = fetchedClusters.filter(c => c.face_count > 0);
dispatch(setClusters(filteredClusters));
}
}, [clustersData, clustersSuccess, dispatch]);
🤖 Prompt for AI Agents
In frontend/src/components/FaceCollections.tsx around lines 23 to 32, remove the
development console.log call, rename the local variable `clusters` to
`fetchedClusters` (or `allClusters`) to avoid shadowing the Redux state
variable, and eliminate the redundant `|| []` fallback since the optional
chaining already guarantees the existence check; then dispatch the filtered
results using the new variable name.

@DecodeX15
Copy link
Copy Markdown
Member Author

@rahulharpal1603 sir please review this

@rahulharpal1603 rahulharpal1603 linked an issue Oct 28, 2025 that may be closed by this pull request
6 tasks
@github-actions github-actions Bot added backend bug Something isn't working labels Oct 31, 2025
Comment on lines +26 to +30

const filteredClusters = clusters.filter((c) => c.face_count > 0);

dispatch(setClusters(filteredClusters));
// console.log(filteredClusters);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the correct way. You need to ensure that empty face clusters are deleted at the database level.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the correct way. You need to ensure that empty face clusters are deleted at the database level.

sir can you please elaborate more ??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sir i will write a route to delete all face id with 0 face count ??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tell me sir ??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sir pleaseeee review

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
frontend/src/components/FaceCollections.tsx (1)

25-30: Good progress on console.log; complete the refactor by addressing variable shadowing.

The filtering logic correctly solves the face count inconsistency issue. The console.log has been commented out (good improvement from the previous review), but two issues from the prior review remain:

  1. Line 25: The local variable clusters shadows the Redux state variable clusters from line 16, reducing code clarity.

  2. Line 25: The || [] fallback is redundant since line 24's optional chaining (clustersData?.data?.clusters) already guarantees the property exists.

Apply this diff to complete the cleanup:

  useEffect(() => {
    if (clustersSuccess && clustersData?.data?.clusters) {
-     const clusters = (clustersData.data.clusters || []) as Cluster[];
+     const fetchedClusters = clustersData.data.clusters as Cluster[];

-     const filteredClusters = clusters.filter((c) => c.face_count > 0);
+     const filteredClusters = fetchedClusters.filter((c) => c.face_count > 0);

      dispatch(setClusters(filteredClusters));
-     // console.log(filteredClusters);
    }
  }, [clustersData, clustersSuccess, dispatch]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae9de04 and 85cb38c.

📒 Files selected for processing (2)
  • docs/backend/backend_python/openapi.json (2 hunks)
  • frontend/src/components/FaceCollections.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Backend Tests
🔇 Additional comments (1)
docs/backend/backend_python/openapi.json (1)

1081-1083: Verify whether these OpenAPI schema changes are intentional.

The changes to this file don't appear to be directly related to the PR objective of fixing face count inconsistency after folder removal:

  1. Line 1081-1083: The input_type parameter now uses a direct $ref with sibling properties (valid in OpenAPI 3.1), but this seems unrelated to the face clustering fix.

  2. Line 2158: Adding "additionalProperties": true to ImageInCluster.Metadata broadens the validation to accept any additional metadata properties.

These look like incidental schema regeneration changes. Please confirm whether these modifications are intentional and necessary for this PR, or if they should be reverted or moved to a separate PR focused on API schema updates.

Also applies to: 2158-2158

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
frontend/src/components/FaceCollections.tsx (1)

25-25: Rename local variable to avoid shadowing Redux state.

The local variable clusters shadows the Redux state variable from line 16, making the code harder to follow and maintain. Consider renaming to fetchedClusters or allClusters.

Apply this diff:

-    const clusters = clustersData.data.clusters as Cluster[];
-    dispatch(setClusters(clusters));
+    const fetchedClusters = clustersData.data.clusters as Cluster[];
+    dispatch(setClusters(fetchedClusters));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85cb38c and 324e5ca.

📒 Files selected for processing (5)
  • backend/app/database/face_clusters.py (2 hunks)
  • backend/app/routes/folders.py (4 hunks)
  • backend/app/utils/face_clusters.py (1 hunks)
  • frontend/src/components/FaceCollections.tsx (1 hunks)
  • frontend/src/components/Media/MediaView.tsx (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/Media/MediaView.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/components/FaceCollections.tsx (1)
frontend/src/types/Media.ts (1)
  • Cluster (51-56)
backend/app/database/face_clusters.py (1)
backend/app/logging/setup_logging.py (2)
  • setup_logging (134-196)
  • get_logger (199-209)
backend/app/routes/folders.py (3)
backend/app/database/folders.py (1)
  • db_delete_folders_batch (155-187)
backend/app/utils/API.py (1)
  • API_util_restart_sync_microservice_watcher (8-33)
backend/app/utils/face_clusters.py (1)
  • cluster_util_delete_empty_clusters (592-624)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Backend Tests
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (4)
backend/app/database/face_clusters.py (1)

6-7: LGTM!

Logger initialization follows the correct pattern.

backend/app/routes/folders.py (2)

80-81: LGTM with a note on error handling.

The placement after API_util_restart_sync_microservice_watcher() is appropriate. Since this runs in a background executor, errors are logged but don't block the API response. However, note that the current implementation silently returns 0 on failure (see review comment on cluster_util_delete_empty_clusters()).


131-132: LGTM!

Consistent placement with the post-folder-add sequence. This executes asynchronously in the background executor.

frontend/src/components/FaceCollections.tsx (1)

23-28: Backend empty cluster deletion is correctly implemented.

Verification confirms the backend properly handles deletion of empty clusters at the database level. The cluster_util_delete_empty_clusters() function is called immediately after db_delete_folders_batch() in the delete-folders endpoint (backend/app/routes/folders.py:334), ensuring orphaned clusters are removed before the frontend fetches updated cluster data. The frontend changes correctly rely on this backend cleanup.

Comment on lines +330 to +352
def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)

deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
finally:
conn.close()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Optimize the subquery and add error handling.

The current implementation has two issues:

  1. Performance concern: The NOT IN subquery can be slow with large datasets because it doesn't leverage indexes efficiently. With many clusters and faces, this could block the database.

  2. Missing error handling: If the query fails, no rollback is performed, potentially leaving the transaction in an inconsistent state.

Apply this diff to improve performance and add error handling:

 def db_delete_empty_clusters() -> int:
     """
     Delete all clusters that have no faces associated with them.
     """
     conn = sqlite3.connect(DATABASE_PATH)
     cursor = conn.cursor()
 
     try:
         cursor.execute(
             """
             DELETE FROM face_clusters
-            WHERE cluster_id NOT IN (
-                SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
+            WHERE NOT EXISTS (
+                SELECT 1 FROM faces 
+                WHERE faces.cluster_id = face_clusters.cluster_id
             )
             """
         )
 
         deleted_count = cursor.rowcount
         conn.commit()
         logger.info(f"Deleted {deleted_count} empty clusters.")
         return deleted_count
+    except Exception as e:
+        logger.error(f"Error deleting empty clusters: {e}")
+        conn.rollback()
+        raise
     finally:
         conn.close()

The NOT EXISTS correlated subquery allows the database to use indexes efficiently and short-circuit as soon as a matching face is found.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
finally:
conn.close()
def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE NOT EXISTS (
SELECT 1 FROM faces
WHERE faces.cluster_id = face_clusters.cluster_id
)
"""
)
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
conn.rollback()
raise
finally:
conn.close()
🤖 Prompt for AI Agents
In backend/app/database/face_clusters.py around lines 330 to 352, replace the
current DELETE ... WHERE cluster_id NOT IN (...) with a correlated NOT EXISTS
subquery to improve query planning and index usage (e.g., DELETE FROM
face_clusters fc WHERE NOT EXISTS (SELECT 1 FROM faces f WHERE f.cluster_id =
fc.cluster_id)); also wrap the execute/commit in a try/except block that calls
conn.rollback() on exception, logs the exception details, and re-raises the
error (ensure conn.close() remains in the finally block).


deleted_count = db_delete_folders_batch(request.folder_ids)

cluster_util_delete_empty_clusters()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider moving cluster cleanup to background processing.

Unlike the post_folder_add_sequence and post_sync_folder_sequence which run cleanup asynchronously in an executor, this call executes synchronously and blocks the API response. If there are many empty clusters to delete, this could cause:

  1. Delayed response to the client
  2. Potential timeout on slow databases
  3. Inconsistent user experience compared to other operations

Option 1 (Recommended): Move to background processing for consistency:

     deleted_count = db_delete_folders_batch(request.folder_ids)
-    cluster_util_delete_empty_clusters()
+    
+    # Clean up empty clusters in background
+    executor: ProcessPoolExecutor = app_state.executor
+    executor.submit(cluster_util_delete_empty_clusters)
+    
     return DeleteFoldersResponse(

Note: You'll need to add app_state=Depends(get_state) to the function signature.

Option 2: Keep synchronous if immediate consistency is required, but document the trade-off and consider adding a timeout.

Which approach is more appropriate depends on whether you need the clusters cleaned up before returning to the client.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/routes/folders.py around line 334, the direct call to
cluster_util_delete_empty_clusters() runs synchronously and can block the API
response; move this cleanup to background processing for consistency with
post_folder_add_sequence/post_sync_folder_sequence by invoking
cluster_util_delete_empty_clusters via the app state executor (e.g.,
app_state.background or run_in_executor) so it runs asynchronously; update the
endpoint signature to accept app_state=Depends(get_state) and schedule the
cleanup task on that executor, keeping the endpoint response immediate.

Comment on lines +592 to +624
def cluster_util_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.

Returns:
int: Number of clusters deleted
"""
import sqlite3
from app.config.settings import DATABASE_PATH

conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)

deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
conn.rollback()
return 0
finally:
conn.close()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Refactor to use the database layer instead of duplicating SQL logic.

This function duplicates the exact same SQL logic from db_delete_empty_clusters() in backend/app/database/face_clusters.py. This creates several issues:

  1. Architecture violation: Utils layer should not contain raw SQL queries; database operations should be encapsulated in the database layer.
  2. Code duplication: Identical SQL in two places makes maintenance harder and increases risk of divergence.
  3. Inconsistent error handling: This version swallows errors and returns 0, while the database version propagates exceptions.

Apply this diff to call the database layer instead:

+from app.database.face_clusters import db_delete_empty_clusters
+
 def cluster_util_delete_empty_clusters() -> int:
     """
     Delete all clusters that have no faces associated with them.
 
     Returns:
         int: Number of clusters deleted
     """
-    import sqlite3
-    from app.config.settings import DATABASE_PATH
-
-    conn = sqlite3.connect(DATABASE_PATH)
-    cursor = conn.cursor()
-
     try:
-        cursor.execute(
-            """
-            DELETE FROM face_clusters
-            WHERE cluster_id NOT IN (
-                SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
-            )
-            """
-        )
-
-        deleted_count = cursor.rowcount
-        conn.commit()
-        logger.info(f"Deleted {deleted_count} empty clusters.")
-        return deleted_count
+        return db_delete_empty_clusters()
     except Exception as e:
         logger.error(f"Error deleting empty clusters: {e}")
-        conn.rollback()
         return 0
-    finally:
-        conn.close()

This maintains the error-swallowing behavior at the utils layer if desired, while eliminating the duplication.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def cluster_util_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
Returns:
int: Number of clusters deleted
"""
import sqlite3
from app.config.settings import DATABASE_PATH
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
conn.rollback()
return 0
finally:
conn.close()
from app.database.face_clusters import db_delete_empty_clusters
def cluster_util_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
Returns:
int: Number of clusters deleted
"""
try:
return db_delete_empty_clusters()
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
return 0
🤖 Prompt for AI Agents
In backend/app/utils/face_clusters.py around lines 592 to 624, replace the
inline SQLite deletion SQL with a call into the database layer: import the
db_delete_empty_clusters function from backend.app.database.face_clusters, call
it from this util, log the returned deleted count and return it; keep the
current try/except/finally shape if you want to continue swallowing errors
(catch Exception, log error, return 0) but remove the sqlite3 connection,
cursor, raw SQL, commit/rollback and conn.close() so DB handling is delegated
entirely to db_delete_empty_clusters.

@DecodeX15
Copy link
Copy Markdown
Member Author

@rahulharpal1603 sir can u please review it

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/main.tsx (1)

6-22: Restore shutdown on window close to avoid orphaned services.

The close handler no longer stops backend/sync processes, which can leave background services running and cause port conflicts on the next launch. Please reintroduce a stop mechanism (e.g., a stopServer that terminates both processes) and invoke it here.

💡 Suggested direction (requires implementing stopServer in serverUtils)
-import { startServer } from './utils/serverUtils';
+import { startServer, stopServer } from './utils/serverUtils';

@@
     const { getCurrentWindow } = await import('@tauri-apps/api/window');
     await getCurrentWindow().onCloseRequested(async () => {
-      // code to stop the server
+      await stopServer();
     });
🤖 Fix all issues with AI agents
In @.pre-commit-config.yaml:
- Around line 2-3: Update the pinned pre-commit tool versions: change the
ruff-pre-commit entry's rev from v0.4.10 to v0.14.14 and update the black
entry's rev to v26.1.0 unless the project must support Python 3.8 or 3.9, in
which case keep the existing black rev (24.4.2); ensure you update the rev
values for the two repo entries referenced
(https://github.com/astral-sh/ruff-pre-commit and the black pre-commit repo) so
the pre-commit config reflects the latest compatible releases.

In `@backend/app/logging/setup_logging.py`:
- Around line 246-255: When re-dispatching the modified LogRecord to other
handlers in setup_logging.py, respect each handler's level instead of calling
handler.handle(record) unconditionally; update the loop over
root_logger.handlers to skip self and only invoke handler.handle(record) when
record.levelno >= handler.level (or equivalently call handler.filter(record) to
apply filters), e.g., in the block iterating root_logger.handlers, add a guard
using handler.level and/or handler.filter before calling handler.handle(record)
so handlers don't receive records below their configured level.

In `@docs/backend/backend_rust/api.md`:
- Around line 17-19: The example has a variable name mismatch and outdated
comment: the code calls invoke("get_resources_folder_path") and assigns to
resourcesFolderPath but logs serverPath; replace the console.log reference to
use resourcesFolderPath (not serverPath) and update the preceding comment text
to accurate terminology (e.g., "Get resources folder path") so the example is
correct and consistent with the invoke call.

In `@scripts/setup.js`:
- Around line 19-44: Documentation and scripts disagree: setup.js (using
os.platform() checks and debianVersionPath/bashScript logic) and setup.sh
currently reject macOS (Darwin) but docs claim macOS is supported; either update
docs to remove macOS claims or add macOS support to the scripts. To fix, choose
one approach: (A) update docs/overview/features.md, docs/Manual_Setup_Guide.md,
and CONTRIBUTING.md to remove or qualify macOS support and clearly state
supported platforms (Windows and Debian-based Linux), or (B) implement macOS
handling in setup.js by adding an os.platform() === 'darwin' branch that detects
macOS, sets command/args appropriately (or invokes setup.sh with
macOS-compatible commands), ensures executable permissions similar to the
debianVersionPath branch, and update setup.sh with a macOS code path; update
logging messages that currently list supported OSes to include or exclude macOS
accordingly.

In `@scripts/setup.sh`:
- Around line 102-110: The script uses an unguarded "cd sync-microservice" so if
the directory change fails subsequent virtualenv and pip commands run in the
wrong place; update the block around the "cd sync-microservice" command to check
the exit status (or use a safe conditional like "cd sync-microservice || { echo
...; exit 1; }") and exit with a non-zero status on failure, ensuring you only
run "python -m venv .sync-env", "source .sync-env/bin/activate", "pip install -r
requirements.txt" and the following "deactivate" when the cd succeeded; apply
the same guard/behavior to the earlier backend setup block that also performs an
unguarded cd.

In `@sync-microservice/app/logging/setup_logging.py`:
- Around line 254-263: The current re-dispatch loop in setup_logging.py bypasses
handler-level filtering by calling handler.handle(record) directly; update the
loop that iterates root_logger.handlers to respect handler filters/levels either
by using root_logger.callHandlers(record) or by guarding each re-dispatch with
handler.level and filters (e.g., for each handler in root_logger.handlers (skip
self) check handler.level and handler.filter(record) / record.levelno >=
handler.level before calling handler.handle(record)), so only handlers that
should accept the record receive it.

In `@sync-microservice/main.py`:
- Around line 50-51: The server is bound to "localhost" which prevents
remote/container access; in main.py locate the server startup call that sets
host and port (the uvicorn.run or equivalent where host="localhost", port=52124)
and change host back to "0.0.0.0" to allow external and container network
access, or if the service must be local-only, add a clear comment/docstring next
to that host setting (and update any deployment docs) stating the intentional
localhost binding instead of changing behavior.
🧹 Nitpick comments (7)
app.py (1)

17-18: Batch the invalidations if the API accepts multiple keys.

This reduces redundant cache client round‑trips and makes the invalidation more atomic.

♻️ Suggested change
-    invalidate_cache("albums:get_all_albums")
-    invalidate_cache("folder_structure:get_folder_structure")
+    invalidate_cache("albums:get_all_albums", "folder_structure:get_folder_structure")
sync-microservice/app/core/lifespan.py (1)

3-57: Avoid blocking the event loop during startup retries.

time.sleep() in an async lifespan blocks the event loop; switch to await asyncio.sleep() and use time.monotonic() for reliable timeout tracking (also counts db check time).

♻️ Proposed fix (non-blocking sleep + monotonic timeout)
-import time
+import asyncio
+import time
@@
-        start_time = time.time()
+        start_time = time.monotonic()
@@
-            elapsed_time = time.time() - start_time
-
             if db_check_database_connection():
                 logger.info(f"Database connection successful on attempt {attempt}")
                 break
+            elapsed_time = time.monotonic() - start_time
@@
-            time.sleep(retry_interval)
+            await asyncio.sleep(retry_interval)
frontend/src-tauri/capabilities/migrated.json (1)

95-99: Verify spawn targets resolve in packaged builds (and PowerShell invocation).

These entries assume ./PictoPy_Server and ./PictoPy_Sync are in the process CWD and that PowerShell will execute them as-is. In packaged Tauri apps that often isn’t true, which can prevent the backend/sync services from starting. Please verify on packaged Windows/macOS/Linux builds; if these are bundled binaries, consider spawning them directly (or as sidecars) and using explicit .exe or -Command on Windows to avoid PowerShell argument parsing issues.

Also applies to: 106-110, 117-121, 128-131

frontend/src/components/OnboardingSteps/ServerCheck.tsx (1)

23-36: Consider whether 60 retries aligns with onboarding UX expectations.

retry: 60 with a fixed retryDelay: 1000 means users wait ~60 seconds before the error path triggers during health checks. Confirm this timeout window is appropriate for initial server connectivity validation, or make the retry budget configurable per environment.

backend/main.py (1)

73-74: Centralize host/port to avoid config drift.
Hard-coding host/port in multiple places risks mismatch and makes non-local deployments harder. Consider sourcing from env/settings and reusing for both OpenAPI + runtime config (and confirm localhost binding is intended outside dev).

💡 Proposed refactor
+BACKEND_HOST = os.getenv("BACKEND_HOST", "localhost")
+BACKEND_PORT = int(os.getenv("BACKEND_PORT", "52123"))
+
 app = FastAPI(
     lifespan=lifespan,
     title="PictoPy",
@@
     servers=[
-        {"url": "http://localhost:52123", "description": "Local Development server"}
+        {
+            "url": f"http://{BACKEND_HOST}:{BACKEND_PORT}",
+            "description": "Local Development server",
+        }
     ],
 )
@@
     config = Config(
         app=app,
-        host="localhost",
-        port=52123,
+        host=BACKEND_HOST,
+        port=BACKEND_PORT,
         log_level="info",
         log_config=None,  # This is crucial - disable Uvicorn's default logging config
     )

Also applies to: 142-145

frontend/src-tauri/src/services/mod.rs (1)

5-10: Add a defensive existence check for the resolved resources path.
If the bundled layout still uses resources/backend, this will silently return a non-existent path and fail later. A quick check provides a clearer error and avoids downstream surprises.

🧩 Proposed guard
 pub fn get_resources_folder_path(handle: tauri::AppHandle) -> Result<String, String> {
     let resource_path = handle
         .path()
         .resolve("resources", BaseDirectory::Resource)
         .map_err(|e| e.to_string())?;
+    if !resource_path.exists() {
+        return Err(format!(
+            "Resources folder not found: {}",
+            resource_path.to_string_lossy()
+        ));
+    }
     Ok(resource_path.to_string_lossy().to_string())
 }
docs/Script_Setup_Guide.md (1)

8-8: Minor style improvement for the note.

Consider rephrasing for better readability.

📝 Suggested improvement
-> Note that the Step No. 8 given below is not mentioned in the video, please follow that step also.
+> Note that Step 8 below is not covered in the video. Please follow that step as well.

Comment thread .pre-commit-config.yaml
Comment on lines +2 to +3
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

latest release astral-sh ruff-pre-commit v0.4.10 current supported

💡 Result:

astral-sh/ruff-pre-commit v0.4.10 is not current.

  • The latest release of astral-sh/ruff-pre-commit is v0.14.14 (Jan 22, 2026). [1]
  • The repository README currently recommends using rev: v0.14.14 in .pre-commit-config.yaml. [2]

So v0.4.10 is outdated; if you want a currently supported/current version, update your rev to v0.14.14. [1][2]

Sources: [1] [2]


🌐 Web query:

latest release psf black 24.4.2 current supported

💡 Result:

  • Latest PSF Black release: 26.1.0 (released Jan 18, 2026). Supported runtime: Python ≥ 3.10 (classifiers include 3.10–3.14). [1][2]
  • Black 24.4.2: released Apr 26, 2024; supported runtime: Python ≥ 3.8 (classifiers 3.8–3.12). It is not the current release. [3]
  • Support changes since 24.4.2: Black dropped Python 3.8 in 24.10.0 and dropped Python 3.9 in 25.12.0 (which is why current Black now requires Python 3.10+). [4]

Sources: [1] [2] [3] [4]


Update pinned tool versions—both are significantly outdated.

  • ruff-pre-commit v0.4.10 is from 2024; the latest release is v0.14.14 (Jan 22, 2026). Update rev to v0.14.14.
  • black 24.4.2 is from April 2024; the latest release is 26.1.0 (Jan 18, 2026). However, note that Black 25.12.0+ requires Python ≥ 3.10 (it dropped Python 3.8 in v24.10.0 and Python 3.9 in v25.12.0). If your project must support Python 3.8 or 3.9, keep 24.4.2; otherwise, update to 26.1.0.

Also applies to: 8-9

🤖 Prompt for AI Agents
In @.pre-commit-config.yaml around lines 2 - 3, Update the pinned pre-commit
tool versions: change the ruff-pre-commit entry's rev from v0.4.10 to v0.14.14
and update the black entry's rev to v26.1.0 unless the project must support
Python 3.8 or 3.9, in which case keep the existing black rev (24.4.2); ensure
you update the rev values for the two repo entries referenced
(https://github.com/astral-sh/ruff-pre-commit and the black pre-commit repo) so
the pre-commit config reflects the latest compatible releases.

Comment on lines +246 to +255
record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None

root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is not self:
handler.handle(record)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Respect handler log levels when re-dispatching records.

Calling handler.handle(record) directly bypasses handler-level filtering, so DEBUG logs can leak into INFO+ handlers. Add a record.levelno >= handler.level guard (or delegate through Logger.callHandlers) to keep log levels consistent.

🛠️ Proposed fix
-        root_logger = logging.getLogger()
-        for handler in root_logger.handlers:
-            if handler is not self:
-                handler.handle(record)
+        root_logger = logging.getLogger()
+        for handler in root_logger.handlers:
+            if handler is self:
+                continue
+            if record.levelno >= handler.level:
+                handler.handle(record)
🤖 Prompt for AI Agents
In `@backend/app/logging/setup_logging.py` around lines 246 - 255, When
re-dispatching the modified LogRecord to other handlers in setup_logging.py,
respect each handler's level instead of calling handler.handle(record)
unconditionally; update the loop over root_logger.handlers to skip self and only
invoke handler.handle(record) when record.levelno >= handler.level (or
equivalently call handler.filter(record) to apply filters), e.g., in the block
iterating root_logger.handlers, add a guard using handler.level and/or
handler.filter before calling handler.handle(record) so handlers don't receive
records below their configured level.

Comment on lines 17 to +19
// Example: Get server path
const serverPath = await invoke("get_server_path");
console.log("Server path:", serverPath);
const resourcesFolderPath = await invoke("get_resources_folder_path");
console.log("Resources folder path:", serverPath);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix variable reference mismatch in example code.

Line 19 references serverPath, but the variable declared on line 18 is resourcesFolderPath. This will cause a ReferenceError if developers copy this example. Additionally, the comment on line 17 uses outdated terminology.

🐛 Proposed fix for variable mismatch and comment
-// Example: Get server path
-const resourcesFolderPath = await invoke("get_resources_folder_path");
-console.log("Resources folder path:", serverPath);
+// Example: Get resources folder path
+const resourcesFolderPath = await invoke("get_resources_folder_path");
+console.log("Resources folder path:", resourcesFolderPath);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Example: Get server path
const serverPath = await invoke("get_server_path");
console.log("Server path:", serverPath);
const resourcesFolderPath = await invoke("get_resources_folder_path");
console.log("Resources folder path:", serverPath);
// Example: Get resources folder path
const resourcesFolderPath = await invoke("get_resources_folder_path");
console.log("Resources folder path:", resourcesFolderPath);
🤖 Prompt for AI Agents
In `@docs/backend/backend_rust/api.md` around lines 17 - 19, The example has a
variable name mismatch and outdated comment: the code calls
invoke("get_resources_folder_path") and assigns to resourcesFolderPath but logs
serverPath; replace the console.log reference to use resourcesFolderPath (not
serverPath) and update the preceding comment text to accurate terminology (e.g.,
"Get resources folder path") so the example is correct and consistent with the
invoke call.

Comment thread scripts/setup.js
Comment on lines +19 to 44
} else if (os.platform() === 'linux') {
// Check if it's Debian-based Linux
const debianVersionPath = '/etc/debian_version';
if (fs.existsSync(debianVersionPath)) {
// On Debian-based Linux, use the bash script
command = bashScript;
args = [];

// Ensure the bash script is executable; if not, set the execute permission.
try {
fs.accessSync(bashScript, fs.constants.X_OK);
} catch (err) {
console.log(`File ${bashScript} is not executable. Setting execute permission...`);
fs.chmodSync(bashScript, 0o755);
}
} else {
console.error('Unsupported Linux distribution. This setup script only supports Debian-based Linux distributions.');
console.error('Please install system dependencies manually and run the individual setup commands.');
process.exit(1);
}
} else {
console.error(`Unsupported operating system: ${os.platform()}`);
console.error('This setup script only supports Windows and Debian-based Linux distributions.');
console.error('Please install system dependencies manually and run the individual setup commands.');
process.exit(1);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full context of setup.js to understand the change
cat -n scripts/setup.js

Repository: AOSSIE-Org/PictoPy

Length of output: 2311


🏁 Script executed:

# Check git history to see if macOS support was previously present
git log --oneline -p scripts/setup.js | head -200

Repository: AOSSIE-Org/PictoPy

Length of output: 2156


🏁 Script executed:

# Search for macOS/Darwin references in documentation
rg -i 'macos|darwin|osx' --type=md

Repository: AOSSIE-Org/PictoPy

Length of output: 482


🏁 Script executed:

# Check if there's a setup.sh and what platforms it supports
cat scripts/setup.sh 2>/dev/null | head -50

Repository: AOSSIE-Org/PictoPy

Length of output: 1512


🏁 Script executed:

# Check for any README or docs that mention platform support
find . -name 'README*' -o -name 'INSTALL*' | head -5

Repository: AOSSIE-Org/PictoPy

Length of output: 174


Documentation claims macOS support that the setup scripts don't provide.

The setup.js file is newly created (not modified from a previous version) and supports only Windows and Debian-based Linux. However, the project documentation claims cross-platform support for macOS:

  • docs/overview/features.md: "Available on major operating systems (Windows, macOS, Linux)"
  • docs/Manual_Setup_Guide.md: Mentions "Terminal (Linux/MacOS)"
  • CONTRIBUTING.md: References "On macOS/Linux"

Both setup.js and setup.sh reject macOS (Darwin) users. Update documentation to accurately reflect that macOS is not supported, or add macOS support to the setup scripts.

🤖 Prompt for AI Agents
In `@scripts/setup.js` around lines 19 - 44, Documentation and scripts disagree:
setup.js (using os.platform() checks and debianVersionPath/bashScript logic) and
setup.sh currently reject macOS (Darwin) but docs claim macOS is supported;
either update docs to remove macOS claims or add macOS support to the scripts.
To fix, choose one approach: (A) update docs/overview/features.md,
docs/Manual_Setup_Guide.md, and CONTRIBUTING.md to remove or qualify macOS
support and clearly state supported platforms (Windows and Debian-based Linux),
or (B) implement macOS handling in setup.js by adding an os.platform() ===
'darwin' branch that detects macOS, sets command/args appropriately (or invokes
setup.sh with macOS-compatible commands), ensures executable permissions similar
to the debianVersionPath branch, and update setup.sh with a macOS code path;
update logging messages that currently list supported OSes to include or exclude
macOS accordingly.

Comment thread scripts/setup.sh
Comment on lines +102 to +110
# ---- Set up the sync-microservice ----
echo -e "${YELLOW}Setting up sync-microservice...${NC}"
cd sync-microservice
python -m venv .sync-env
source .sync-env/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
deactivate
cd ..
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for directory change.

If the cd sync-microservice command fails (e.g., directory doesn't exist), subsequent commands will execute in the wrong directory, potentially corrupting the parent environment.

Proposed fix with error handling
 # ---- Set up the sync-microservice ----
 echo -e "${YELLOW}Setting up sync-microservice...${NC}"
-cd sync-microservice
+cd sync-microservice || { echo -e "${RED}sync-microservice directory not found${NC}"; exit 1; }
 python -m venv .sync-env
 source .sync-env/bin/activate
 pip install --upgrade pip
 pip install -r requirements.txt
 deactivate
-cd ..
+cd .. || exit 1

Note: The existing backend setup block (lines 94-100) has the same issue—consider applying similar error handling there for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ---- Set up the sync-microservice ----
echo -e "${YELLOW}Setting up sync-microservice...${NC}"
cd sync-microservice
python -m venv .sync-env
source .sync-env/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
deactivate
cd ..
# ---- Set up the sync-microservice ----
echo -e "${YELLOW}Setting up sync-microservice...${NC}"
cd sync-microservice || { echo -e "${RED}sync-microservice directory not found${NC}"; exit 1; }
python -m venv .sync-env
source .sync-env/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
deactivate
cd .. || exit 1
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 104-104: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)

🤖 Prompt for AI Agents
In `@scripts/setup.sh` around lines 102 - 110, The script uses an unguarded "cd
sync-microservice" so if the directory change fails subsequent virtualenv and
pip commands run in the wrong place; update the block around the "cd
sync-microservice" command to check the exit status (or use a safe conditional
like "cd sync-microservice || { echo ...; exit 1; }") and exit with a non-zero
status on failure, ensuring you only run "python -m venv .sync-env", "source
.sync-env/bin/activate", "pip install -r requirements.txt" and the following
"deactivate" when the cd succeeded; apply the same guard/behavior to the earlier
backend setup block that also performs an unguarded cd.

Comment on lines +254 to +263
record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None

root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is not self:
handler.handle(record)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Respect handler log levels when re-dispatching records.

Same issue as backend: calling handler.handle(record) directly skips handler-level filtering, so lower-level logs can leak into higher-level handlers. Add a level guard (or use Logger.callHandlers) to preserve expected log behavior.

🛠️ Proposed fix
-        root_logger = logging.getLogger()
-        for handler in root_logger.handlers:
-            if handler is not self:
-                handler.handle(record)
+        root_logger = logging.getLogger()
+        for handler in root_logger.handlers:
+            if handler is self:
+                continue
+            if record.levelno >= handler.level:
+                handler.handle(record)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None
root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is not self:
handler.handle(record)
record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None
root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is self:
continue
if record.levelno >= handler.level:
handler.handle(record)
🤖 Prompt for AI Agents
In `@sync-microservice/app/logging/setup_logging.py` around lines 254 - 263, The
current re-dispatch loop in setup_logging.py bypasses handler-level filtering by
calling handler.handle(record) directly; update the loop that iterates
root_logger.handlers to respect handler filters/levels either by using
root_logger.callHandlers(record) or by guarding each re-dispatch with
handler.level and filters (e.g., for each handler in root_logger.handlers (skip
self) check handler.level and handler.filter(record) / record.levelno >=
handler.level before calling handler.handle(record)), so only handlers that
should accept the record receive it.

Comment thread sync-microservice/main.py
Comment on lines +50 to +51
host="localhost",
port=52124,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Uvicorn host localhost vs 0.0.0.0 Docker networking difference

💡 Result:

In Uvicorn, --host controls which network interface the server binds to inside the container:

  • --host localhost (usually 127.0.0.1) binds to the container’s loopback interface, so the app is reachable only from inside that same container. Even if you run Docker with -p 8000:8000, the host can’t reach it because nothing is listening on the container’s external interface. [1][2]
  • --host 0.0.0.0 binds to all interfaces in the container, including the one Docker connects to the bridge network, so port publishing (-p) can forward traffic from the host to the container successfully. [1][2][3]

How Docker port publishing relates:

  • docker run -p HOSTPORT:CONTAINERPORT ... publishes the container port on the host, and by default Docker binds that published port on all host interfaces (0.0.0.0 / ::). [4]
  • You can restrict exposure to only the host machine by binding on the host side: -p 127.0.0.1:8000:8000. This is independent of Uvicorn, but Uvicorn still must bind 0.0.0.0 inside the container to accept forwarded traffic. [4]

Typical Docker command:

uvicorn main:app --host 0.0.0.0 --port 8000
docker run -p 8000:8000 your-image

Sources: [1] StackOverflow answer on FastAPI/Uvicorn in Docker, [2] explanation of binding to 0.0.0.0 for container reachability, [3] tiangolo image docs on HOST=127.0.0.1 only being available inside container, [4] Docker Docs on port publishing/default bind address.


Binding to localhost will prevent remote and container access.

The change from "0.0.0.0" to "localhost" binds the server to the loopback interface only. This breaks:

  • Docker port publishing (even with -p 8000:8000, the forwarded traffic cannot reach the service)
  • Access from other containers in the same network
  • Any remote or VMs accessing the service

If remote or containerized access is needed, revert to "0.0.0.0". If this service is genuinely local-only, document this design decision and ensure no remote access is required.

🤖 Prompt for AI Agents
In `@sync-microservice/main.py` around lines 50 - 51, The server is bound to
"localhost" which prevents remote/container access; in main.py locate the server
startup call that sets host and port (the uvicorn.run or equivalent where
host="localhost", port=52124) and change host back to "0.0.0.0" to allow
external and container network access, or if the service must be local-only, add
a clear comment/docstring next to that host setting (and update any deployment
docs) stating the intentional localhost binding instead of changing behavior.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 3, 2026

⚠️ This PR has merge conflicts.

Please resolve the merge conflicts before review.

Your PR will only be reviewed by a maintainer after all conflicts have been resolved.

📺 Watch this video to understand why conflicts occur and how to resolve them:
https://www.youtube.com/watch?v=Sqsz1-o7nXk

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/routes/folders.py (1)

37-54: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove duplicate imports.

Lines 52-54 duplicate imports already present on lines 37-41 and 40. This violates PEP 8 and creates confusion about which import statements are authoritative.

🔧 Fix: Remove the duplicate import block
 from app.utils.images import (
     image_util_process_folder_images,
     image_util_process_untagged_images,
 )
-from app.utils.model_bootstrap import ensure_ai_tagging_models
-from app.utils.face_clusters import cluster_util_face_clusters_sync
-from app.utils.API import API_util_restart_sync_microservice_watcher
 
 # Initialize logger
 logger = get_logger(__name__)

Note: ensure_ai_tagging_models should be added to the existing import block at lines 48-51 instead:

 from app.utils.images import (
     image_util_process_folder_images,
     image_util_process_untagged_images,
 )
+from app.utils.model_bootstrap import ensure_ai_tagging_models
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/routes/folders.py` around lines 37 - 54, The file has duplicate
import statements: remove the repeated import block that re-imports
cluster_util_face_clusters_sync and API_util_restart_sync_microservice_watcher;
instead consolidate imports so each symbol is imported only once and add
ensure_ai_tagging_models into the existing image/model import block (keep
folder_util_*, image_util_*, and ensure_ai_tagging_models together) to satisfy
PEP8 and avoid duplicate names (look for ensure_ai_tagging_models,
cluster_util_face_clusters_sync, API_util_restart_sync_microservice_watcher to
locate the duplicated imports).
♻️ Duplicate comments (1)
backend/app/routes/folders.py (1)

338-338: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Consider moving cluster cleanup to background processing.

This synchronous call to cluster_util_delete_empty_clusters() is inconsistent with the pattern used in post_folder_add_sequence (line 84) and post_sync_folder_sequence (line 136), where cleanup runs asynchronously via the executor. Blocking the API response could delay the client if many empty clusters exist.

♻️ Move to background processing for consistency
 def delete_folders(request: DeleteFoldersRequest, app_state=Depends(get_state)):
     """Delete multiple folders by their IDs."""
     try:
         if not request.folder_ids:
             raise ValueError("No folder IDs provided")
 
         deleted_count = db_delete_folders_batch(request.folder_ids)
-        cluster_util_delete_empty_clusters()
+        
+        # Clean up empty clusters in background
+        executor: ProcessPoolExecutor = app_state.executor
+        executor.submit(cluster_util_delete_empty_clusters)
+        
         return DeleteFoldersResponse(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/routes/folders.py` at line 338, The synchronous call to
cluster_util_delete_empty_clusters() should be executed in the background like
in post_folder_add_sequence and post_sync_folder_sequence to avoid blocking the
API; replace the direct call with submitting the function to the same executor
(e.g., executor.submit(cluster_util_delete_empty_clusters) or the app's
background task runner) so cleanup runs asynchronously, preserving any existing
error handling/logging used in the other two handlers.
🧹 Nitpick comments (4)
frontend/src/utils/serverUtils.ts (4)

9-14: 💤 Low value

Simplify conditional logic.

The if-else block can be simplified by directly returning response.ok, which already evaluates to a boolean.

♻️ Simplified version
-    if (response.ok) {
+    if (response.ok) {
       console.log('Server is Running!');
-      return true;
-    } else {
-      return false;
     }
+    return response.ok;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 9 - 14, Replace the explicit
if-else that checks response.ok with a direct return of the boolean; locate the
code in frontend/src/utils/serverUtils.ts where response.ok is evaluated (the
block containing "if (response.ok) { console.log('Server is Running!'); return
true } else { return false }") and change it to simply return response.ok,
keeping or moving the console.log as needed.

53-55: 💤 Low value

Consider attaching stdout listener for complete logging.

Only stderr is being monitored, but important startup messages and logs might be written to stdout. Consider attaching a listener to both streams for comprehensive monitoring.

📝 Suggested addition
     const backendChild = await backendCommand.spawn();
+    backendCommand.stdout.on('data', (line) =>
+      console.log('Backend:', line),
+    );
     backendCommand.stderr.on('data', (line) =>
       console.error('Backend Error:', line),
     );

Apply similar logic for the sync service.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 53 - 55, The current spawn
listener only captures stderr via backendCommand.stderr.on('data', ...) and
misses important stdout logs; add a corresponding
backendCommand.stdout.on('data', ...) handler that logs stdout (e.g., as info)
and mirror the same pattern for the sync service command (the equivalent
stdout/stderr listeners you added for backendCommand should be applied to the
sync service process variable) so both streams are monitored and logged
consistently.

44-72: ⚡ Quick win

Consider parallelizing service startup for faster initialization.

Currently, services are started sequentially: backend first, then sync service. Since they appear to be independent, you could spawn both processes concurrently using Promise.all() to reduce total startup time.

⚡ Suggested parallel startup
export const startServer = async (): Promise<void> => {
  try {
    console.log('Starting services!');
    
    const resourcesFolderPath: string = await invoke('get_resources_folder_path');
    
    const startupPromises = [];
    
    // Prepare backend startup
    if (!(await isServerRunning())) {
      startupPromises.push(
        (async () => {
          const backendCommand = Command.create(
            isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
            '',
            { cwd: `${resourcesFolderPath}/backend` },
          );
          const backendChild = await backendCommand.spawn();
          backendCommand.stderr.on('data', (line) =>
            console.error('Backend Error:', line),
          );
          console.log('Backend server started with PID:', backendChild.pid);
          return { service: 'backend', pid: backendChild.pid };
        })()
      );
    }
    
    // Prepare sync service startup
    if (!(await isSyncServiceRunning())) {
      startupPromises.push(
        (async () => {
          const syncCommand = Command.create(
            isWindows() ? 'StartSyncServiceWindows' : 'StartSyncServiceUnix',
            '',
            { cwd: `${resourcesFolderPath}/sync-microservice` },
          );
          const syncChild = await syncCommand.spawn();
          syncCommand.stderr.on('data', (line) =>
            console.error('Sync Service Error:', line),
          );
          console.log('Sync service started with PID:', syncChild.pid);
          return { service: 'sync', pid: syncChild.pid };
        })()
      );
    }
    
    await Promise.all(startupPromises);
    console.log('All services started successfully');
  } catch (error) {
    console.error('Error starting services:', error);
  }
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 44 - 72, The startup
currently spawns backend and sync sequentially; modify startServer so the
independent checks and spawns run concurrently by collecting startup promises
and awaiting Promise.all; specifically, keep using isServerRunning and
isSyncServiceRunning to decide whether to start, but instead of awaiting each
spawn inline, push async IIFEs that call Command.create(...) and await spawn()
(and attach stderr handlers) into an array and then await
Promise.all(startupPromises); ensure you still log each child.pid after spawn
and handle errors from the overall startup with the existing try/catch.

49-49: ⚡ Quick win

Use proper path joining instead of string concatenation.

String concatenation for file paths can lead to issues with separators across platforms. Consider using a path utility or ensuring the resourcesFolderPath doesn't have a trailing slash.

🛤️ Suggested approach
     const backendCommand = Command.create(
       isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
       '',
-      { cwd: resourcesFolderPath + '/backend' },
+      { cwd: `${resourcesFolderPath}/backend` },
     );
     
     // ...
     
     const syncCommand = Command.create(
       isWindows() ? 'StartSyncServiceWindows' : 'StartSyncServiceUnix',
       '',
-      { cwd: resourcesFolderPath + '/sync-microservice' },
+      { cwd: `${resourcesFolderPath}/sync-microservice` },
     );

Or better yet, if Tauri provides path utilities, use those for robust path resolution.

Also applies to: 64-64

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` at line 49, Replace the
string-concatenated paths using resourcesFolderPath with a proper path join
utility: import and use path.join (or Tauri's path utility) to build the cwd
values instead of resourcesFolderPath + '/backend' (and the similar occurrence
at the other spot); update the two places that set { cwd: resourcesFolderPath +
'/backend' } and the one at line 64 to { cwd: path.join(resourcesFolderPath,
'backend') } (and analogous join for the other folder) so path separators are
handled cross-platform.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app.py`:
- Around line 6-10: initialize_app() is defined and calls invalidate_cache() but
is never wired into FastAPI startup so cache invalidation never runs; either
register initialize_app() with the application's startup event (e.g., attach it
via `@app.on_event`("startup") or call it from the existing lifespan manager) or
remove the unused function and its invalidate_cache() call; locate
initialize_app() in app.py and update the app startup wiring (or remove the
function) and ensure any changes are invoked from the same startup path used by
backend/main.py and sync-microservice/main.py.

In `@frontend/src/components/Media/MediaView.tsx`:
- Around line 100-107: The handleOpenFolder function currently contains fully
commented-out logic (references: handleOpenFolder, currentImage,
revealItemInDir); either delete the commented block entirely if the feature is
permanently removed, or replace the block with an inline TODO/FIXME comment
explaining why revealItemInDir is disabled, who/when it will be re-enabled, and
keep a one-line stub (e.g., return) so the linting/tests remain clean; ensure
any references to currentImage?.path remain correct when uncommenting later.

In `@frontend/src/utils/serverUtils.ts`:
- Around line 36-76: startServer currently can leave the backend running if the
sync service fails; add cleanup/rollback so partial startups are torn down on
error: inside startServer track started services (e.g., push {name:'backend',
pid: backendChild.pid} when backendCommand.spawn() succeeds and similarly for
syncChild), wrap orchestration in try/catch, and on catch iterate the
startedServices to gracefully stop/kill each (using the same Command instance or
Tauri kill/stop mechanism), log any cleanup errors, and rethrow the original
error; reference startServer, isServerRunning, isSyncServiceRunning,
backendCommand, syncCommand, backendChild and syncChild to locate where to add
the startedServices array and cleanup code.
- Line 36: The exported function startServer lacks an explicit return type;
update its signature to include a concrete TypeScript return type (e.g.,
Promise<void> if it doesn't return a value, or Promise<http.Server> /
Promise<YourServerType> if it returns a server instance) instead of using any or
leaving it implicit—locate the startServer declaration and add the appropriate
Promise<...> annotation that matches its implementation.
- Around line 6-19: The isServerRunning function lacks explicit TypeScript types
for the fetched response and the caught error; update the signature to keep
Promise<boolean> and add explicit types inside the function by annotating
response as Response (const response: Response = await fetch(...)) and typing
the catch parameter as unknown (catch (error: unknown)), then narrow the unknown
before logging (e.g., if (error instanceof Error) use error.message else
String(error)) so there are no implicit any types while preserving the existing
control flow.
- Around line 21-34: Refactor the duplicated health-check logic by extracting a
reusable helper (e.g., checkServiceHealth) that takes the base URL and an
optional service name, moves the try/catch/fetch/response.ok logic out of
isSyncServiceRunning and isServerRunning, and updates those functions to call
this helper; ensure checkServiceHealth returns a boolean, logs a meaningful
message using the provided service name, and rethrows or handles errors
consistently so both isSyncServiceRunning and isServerRunning simply forward the
boolean result.
- Around line 45-57: After spawning the backend via Command.create(...) and
backendCommand.spawn(), add a readiness check that polls isServerRunning() (or
the backend health endpoint) with a timeout and retry interval before
proceeding; if readiness fails, kill the spawned process (backendChild.pid) and
surface an error via backendCommand.stderr/error logging. Apply the same
polling/timeout pattern for the sync service; use the existing symbols
isServerRunning, Command.create, backendCommand.spawn, backendCommand.stderr,
backendChild.pid, resourcesFolderPath and the
StartBackendWindows/StartBackendUnix commands to locate where to insert the
verification and cleanup logic.
- Line 8: The fetch call that assigns const response = await fetch(BACKEND_URL +
'/health') needs a timeout to avoid hanging; wrap the request with an
AbortController, create a timer (e.g., via setTimeout) that calls
controller.abort() after the desired timeout, pass controller.signal to fetch,
and clear the timer (clearTimeout) after fetch completes; also handle the abort
case (AbortError) or rejected promise so the caller receives a clear timeout
error instead of an indefinite hang.
- Line 4: Replace the unreliable navigator.platform check in isWindows with
Tauri’s OS API: add the `@tauri-apps/plugin-os` dependency to the frontend package
manifest and add the corresponding tauri-plugin-os dependency and plugin
registration in the Rust side (Cargo.toml and tauri main registration) so the
plugin is available at runtime; then update serverUtils.ts to import platform()
from '`@tauri-apps/plugin-os`' and make isWindows an async function that awaits
platform() and returns a boolean when the returned platform string indicates
Windows (e.g., equals or startsWith 'windows').

---

Outside diff comments:
In `@backend/app/routes/folders.py`:
- Around line 37-54: The file has duplicate import statements: remove the
repeated import block that re-imports cluster_util_face_clusters_sync and
API_util_restart_sync_microservice_watcher; instead consolidate imports so each
symbol is imported only once and add ensure_ai_tagging_models into the existing
image/model import block (keep folder_util_*, image_util_*, and
ensure_ai_tagging_models together) to satisfy PEP8 and avoid duplicate names
(look for ensure_ai_tagging_models, cluster_util_face_clusters_sync,
API_util_restart_sync_microservice_watcher to locate the duplicated imports).

---

Duplicate comments:
In `@backend/app/routes/folders.py`:
- Line 338: The synchronous call to cluster_util_delete_empty_clusters() should
be executed in the background like in post_folder_add_sequence and
post_sync_folder_sequence to avoid blocking the API; replace the direct call
with submitting the function to the same executor (e.g.,
executor.submit(cluster_util_delete_empty_clusters) or the app's background task
runner) so cleanup runs asynchronously, preserving any existing error
handling/logging used in the other two handlers.

---

Nitpick comments:
In `@frontend/src/utils/serverUtils.ts`:
- Around line 9-14: Replace the explicit if-else that checks response.ok with a
direct return of the boolean; locate the code in
frontend/src/utils/serverUtils.ts where response.ok is evaluated (the block
containing "if (response.ok) { console.log('Server is Running!'); return true }
else { return false }") and change it to simply return response.ok, keeping or
moving the console.log as needed.
- Around line 53-55: The current spawn listener only captures stderr via
backendCommand.stderr.on('data', ...) and misses important stdout logs; add a
corresponding backendCommand.stdout.on('data', ...) handler that logs stdout
(e.g., as info) and mirror the same pattern for the sync service command (the
equivalent stdout/stderr listeners you added for backendCommand should be
applied to the sync service process variable) so both streams are monitored and
logged consistently.
- Around line 44-72: The startup currently spawns backend and sync sequentially;
modify startServer so the independent checks and spawns run concurrently by
collecting startup promises and awaiting Promise.all; specifically, keep using
isServerRunning and isSyncServiceRunning to decide whether to start, but instead
of awaiting each spawn inline, push async IIFEs that call Command.create(...)
and await spawn() (and attach stderr handlers) into an array and then await
Promise.all(startupPromises); ensure you still log each child.pid after spawn
and handle errors from the overall startup with the existing try/catch.
- Line 49: Replace the string-concatenated paths using resourcesFolderPath with
a proper path join utility: import and use path.join (or Tauri's path utility)
to build the cwd values instead of resourcesFolderPath + '/backend' (and the
similar occurrence at the other spot); update the two places that set { cwd:
resourcesFolderPath + '/backend' } and the one at line 64 to { cwd:
path.join(resourcesFolderPath, 'backend') } (and analogous join for the other
folder) so path separators are handled cross-platform.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db1434fa-45f6-4ed0-a2bd-f0a2f9ed6d19

📥 Commits

Reviewing files that changed from the base of the PR and between 1045902 and 384667c.

📒 Files selected for processing (5)
  • app.py
  • backend/app/routes/folders.py
  • docs/backend/backend_python/openapi.json
  • frontend/src/components/Media/MediaView.tsx
  • frontend/src/utils/serverUtils.ts

Comment thread app.py
Comment on lines +6 to +10
def initialize_app():
# ...existing code...
# Clear any stale cache data on startup
invalidate_cache()
# ...existing code...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if initialize_app is registered as a startup event handler

# Search for startup event registration patterns
rg -n -A 2 -B 2 'initialize_app|on_event.*startup|lifespan' app.py

# Also check if it's called directly in the main application setup
rg -n 'initialize_app\(\)' app.py

Repository: AOSSIE-Org/PictoPy

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== app.py (with line numbers) ==="
nl -ba app.py | sed -n '1,200p'

echo
echo "=== Repo-wide usage of initialize_app ==="
rg -n "initialize_app\s*\(" .

echo
echo "=== Repo-wide FastAPI startup/lifespan wiring ==="
rg -n "on_event\s*\(\s*['\"]startup['\"]\s*\)|lifespan\s*=" .
rg -n "async\s+def\s+lifespan\b|def\s+lifespan\b|`@app`\.on_event\(\s*['\"]startup['\"]\s*\)" .

Repository: AOSSIE-Org/PictoPy

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== app.py (with line numbers) ==="
awk '{printf "%4d-%s\n", NR, $0}' app.py | sed -n '1,220p'

echo
echo "=== Repo-wide usage of initialize_app ==="
rg -n "initialize_app\s*\(" .

echo
echo "=== Repo-wide FastAPI startup/lifespan wiring ==="
rg -n "on_event\s*\(\s*['\"]startup['\"]\s*\)|lifespan\s*=" .
rg -n "async\s+def\s+lifespan\b|def\s+lifespan\b|`@app`\.on_event\(\s*['\"]startup['\"]\s*\)" .

Repository: AOSSIE-Org/PictoPy

Length of output: 1171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Repo-wide references to initialize_app (with/without parentheses) ==="
rg -n "initialize_app" .

echo
echo "=== backend/main.py around lifespan wiring ==="
awk 'NR>=40 && NR<=120 {printf "%4d-%s\n", NR, $0}' backend/main.py

echo
echo "=== backend/main.py lifespan implementation ==="
awk 'NR>=1 && NR<=140 {print}' backend/main.py | head -n 160 | tail -n 120

echo
echo "=== sync-microservice/main.py around lifespan wiring ==="
awk 'NR>=1 && NR<=120 {printf "%4d-%s\n", NR, $0}' sync-microservice/main.py

echo
echo "=== sync-microservice/app/core/lifespan.py ==="
awk 'NR>=1 && NR<=120 {printf "%4d-%s\n", NR, $0}' sync-microservice/app/core/lifespan.py

echo
echo "=== backend (if any) lifespan module ==="
fd -e py -a backend | head -n 5
rg -n "async\s+def\s+lifespan\b|def\s+lifespan\b" backend -S || true

Repository: AOSSIE-Org/PictoPy

Length of output: 12305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== app.py line count ==="
wc -l app.py

echo
echo "=== app.py tail (last 80 lines or fewer) ==="
lines=$(wc -l < app.py)
start=$(( lines>80 ? lines-80+1 : 1 ))
awk -v s="$start" '{if(NR>=s) printf "%4d-%s\n", NR, $0}' app.py

echo
echo "=== app.py startup/lifespan related patterns (repo-local) ==="
rg -n "FastAPI\s*\(|lifespan\s*=|on_event\s*\(\s*['\"]startup['\"]|startup" app.py || true

Repository: AOSSIE-Org/PictoPy

Length of output: 969


Wire initialize_app() into the app startup (or remove unused code)

app.py defines initialize_app() and calls invalidate_cache(), but it isn’t registered with FastAPI startup (@app.on_event("startup")) or lifespan in app.py. Repo-wide, initialize_app() is only defined and never referenced. The running services use their own lifespan setup (backend/main.py and sync-microservice/main.py), so cache invalidation on startup won’t occur unless you connect initialize_app() into that startup path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.py` around lines 6 - 10, initialize_app() is defined and calls
invalidate_cache() but is never wired into FastAPI startup so cache invalidation
never runs; either register initialize_app() with the application's startup
event (e.g., attach it via `@app.on_event`("startup") or call it from the existing
lifespan manager) or remove the unused function and its invalidate_cache() call;
locate initialize_app() in app.py and update the app startup wiring (or remove
the function) and ensure any changes are invoked from the same startup path used
by backend/main.py and sync-microservice/main.py.

Comment on lines 100 to 107
const handleOpenFolder = async () => {
if (!currentImage?.path) return;
try {
await revealItemInDir(currentImage.path);
} catch (err) {
console.error('Failed to open folder:', err);
}
// if (!currentImage?.path) return;
// try {
// await revealItemInDir(currentImage.path);
// } catch (err) {
// console.error('Failed to open folder:', err);
// }
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Remove commented-out code or add a TODO explaining why it's disabled.

The entire implementation of handleOpenFolder has been commented out, effectively disabling the "reveal in folder" feature. Commented-out code should either be removed entirely or include a TODO/FIXME comment explaining why it's temporarily disabled and when it will be re-enabled.

♻️ Suggested approaches

Option 1: Remove if permanently disabled

- /** Opens the system file explorer at the current image's location. */
- const handleOpenFolder = async () => {
-   // if (!currentImage?.path) return;
-   // try {
-   //   await revealItemInDir(currentImage.path);
-   // } catch (err) {
-   //   console.error('Failed to open folder:', err);
-   // }
- };
+ // Note: "Open Folder" feature temporarily disabled - see issue `#XXX`
+ const handleOpenFolder = async () => {
+   // Feature disabled
+ };

Option 2: Add TODO if temporarily disabled

  /** Opens the system file explorer at the current image's location. */
  const handleOpenFolder = async () => {
+   // TODO: Re-enable folder reveal functionality
+   // Disabled due to [reason] - see issue `#XXX`
    // if (!currentImage?.path) return;
    // try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleOpenFolder = async () => {
if (!currentImage?.path) return;
try {
await revealItemInDir(currentImage.path);
} catch (err) {
console.error('Failed to open folder:', err);
}
// if (!currentImage?.path) return;
// try {
// await revealItemInDir(currentImage.path);
// } catch (err) {
// console.error('Failed to open folder:', err);
// }
};
const handleOpenFolder = async () => {
// TODO: Re-enable folder reveal functionality
// Disabled due to [reason] - see issue `#XXX`
// if (!currentImage?.path) return;
// try {
// await revealItemInDir(currentImage.path);
// } catch (err) {
// console.error('Failed to open folder:', err);
// }
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Media/MediaView.tsx` around lines 100 - 107, The
handleOpenFolder function currently contains fully commented-out logic
(references: handleOpenFolder, currentImage, revealItemInDir); either delete the
commented block entirely if the feature is permanently removed, or replace the
block with an inline TODO/FIXME comment explaining why revealItemInDir is
disabled, who/when it will be re-enabled, and keep a one-line stub (e.g.,
return) so the linting/tests remain clean; ensure any references to
currentImage?.path remain correct when uncommenting later.

Comment thread frontend/src/utils/serverUtils.ts Outdated
import { Command } from '@tauri-apps/plugin-shell';
import { invoke } from '@tauri-apps/api/core';
import { BACKEND_URL, SYNC_MICROSERVICE_URL } from '@/config/Backend.ts';
const isWindows = () => navigator.platform.startsWith('Win');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Is navigator.platform deprecated and what is the recommended alternative for Tauri applications?

💡 Result:

Yes—navigator.platform is discouraged / effectively deprecated for web OS detection, and you should avoid using it. For Tauri apps, the recommended alternative is to query the OS information through Tauri’s own APIs (e.g., @tauri-apps/api/plugins/os or @tauri-apps/api/os), rather than relying on navigator.platform. Tauri exposes an explicit platform function that returns the OS as one of a fixed set of values (like 'linux', 'darwin', 'win32', etc.).[1] For completeness on the web side: the MDN guidance for Navigator.platform explains that platform information is unreliable and not recommended due to user-agent/platform reduction and browser detection concerns, and it recommends feature detection instead.[2] References: MDN Navigator.platform reliability guidance [2]; Tauri OS platform API type/signature [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show relevant lines
ls -l frontend/src/utils/serverUtils.ts 2>/dev/null || true
sed -n '1,120p' frontend/src/utils/serverUtils.ts 2>/dev/null || true

# Check for tauri OS plugin usage / dependencies
ls -l package.json 2>/dev/null || true
ls -l frontend/package.json 2>/dev/null || true

# Prefer root package.json then frontend package.json
if [ -f package.json ]; then
  echo "---- root package.json dependencies ----"
  cat package.json | sed -n '1,200p'
fi
if [ -f frontend/package.json ]; then
  echo "---- frontend package.json dependencies ----"
  cat frontend/package.json | sed -n '1,220p'
fi

# Search for plugin-os / api/os usage in repo
rg -n "plugin-os|`@tauri-apps/plugin-os`|`@tauri-apps/api/os`|`@tauri-apps/api/plugins/os`|platform\\(" -S . || true

Repository: AOSSIE-Org/PictoPy

Length of output: 7072


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show relevant lines
if [ -f frontend/src/utils/serverUtils.ts ]; then
  echo "---- serverUtils.ts (first 120 lines) ----"
  sed -n '1,120p' frontend/src/utils/serverUtils.ts
else
  echo "Missing: frontend/src/utils/serverUtils.ts"
fi

# Check for tauri OS plugin usage / dependencies
if [ -f package.json ]; then
  echo "---- root package.json (first 220 lines) ----"
  sed -n '1,220p' package.json
fi
if [ -f frontend/package.json ]; then
  echo "---- frontend/package.json (first 220 lines) ----"
  sed -n '1,220p' frontend/package.json
fi

# Search for OS plugin usage and platform() usage
rg -n "plugin-os|`@tauri-apps/plugin-os`|`@tauri-apps/api/os`|`@tauri-apps/api/plugins/os`|\\bplatform\\(" -S . || true

Repository: AOSSIE-Org/PictoPy

Length of output: 6910


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show relevant lines
if [ -f frontend/src/utils/serverUtils.ts ]; then
  echo "---- serverUtils.ts (first 120 lines) ----"
  sed -n '1,120p' frontend/src/utils/serverUtils.ts
else
  echo "Missing: frontend/src/utils/serverUtils.ts"
fi

# Check for tauri OS plugin usage / dependencies
if [ -f package.json ]; then
  echo "---- root package.json (first 220 lines) ----"
  sed -n '1,220p' package.json
fi
if [ -f frontend/package.json ]; then
  echo "---- frontend/package.json (first 220 lines) ----"
  sed -n '1,220p' frontend/package.json
fi

# Search for OS plugin usage and platform() usage
rg -n "plugin-os|`@tauri-apps/plugin-os`|`@tauri-apps/api/os`|`@tauri-apps/api/plugins/os`|\bplatform\(" -S .

Repository: AOSSIE-Org/PictoPy

Length of output: 6910


🌐 Web query:

Tauri v2 JavaScript API platform() import path @tauri-apps/api/osor@tauri-apps/api/plugins/os``

💡 Result:

In Tauri v2, the OS “platform” JavaScript API comes from the dedicated plugin package, not from @tauri-apps/api/os. 1) Correct import (plugin-os) Use: - import { platform } from '@tauri-apps/plugin-os' Then call: - const platformName = platform(); The platform() function “returns a string describing the specific operating system in use” and its values are set at compile time (e.g. 'linux', 'macos', 'windows', etc.) [1]. 2) Notes about @tauri-apps/api/os The @tauri-apps/api docs describe it as a namespace that exposes other modules, but the authoritative OS “platform” API in v2 is documented under the @tauri-apps/plugin-os reference page (not under @tauri-apps/api/os) [2][1]. 3) Why “plugin setup” matters To use the JS guest bindings for @tauri-apps/plugin-os, you must register the core plugin in your Rust main with tauri_plugin_os::init() [3]. If you tell me whether you’re using Rust desktop Tauri v2 only, or also want mobile support / “vanilla JS without bundler” usage, I can provide the exact end-to-end snippet for your setup.

Citations:


🌐 Web query:

@tauri-apps/plugin-os platform() return type Promise<string> or string

💡 Result:

@tauri-apps/plugin-os platform return type: string (synchronous), not Promise. In Tauri v2, the plugin’s JS API defines platform as returning Platform (a string union) directly: “function platform: Platform”.[1] The type declaration for the plugin also shows “declare function platform: Platform;” (no Promise wrapper).[2] The platform information page likewise presents it as a value you can call immediately (“platform”), consistent with a synchronous return.[3] Note: In Tauri v1, the older @tauri-apps/api/os platform is Promise-based (“platform: Promise”).[4] That differs from @tauri-apps/plugin-os in v2.[1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for Rust initialization of tauri-plugin-os
rg -n "tauri_plugin_os::init\\(\\)|plugin_os|tauri-plugin-os" -S . || true

# Confirm whether frontend has `@tauri-apps/plugin-os` dependency declared anywhere
rg -n "\"`@tauri-apps/plugin-os`\"" -S package.json frontend/package.json **/package.json 2>/dev/null || true

Repository: AOSSIE-Org/PictoPy

Length of output: 44


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List likely Tauri Rust directories
ls -la . 2>/dev/null || true
fd -t d -a 2>/dev/null . -d 3 -E node_modules --prune -x echo {} | head -n 200 || true

# Find tauri config and rust sources
fd -t f "tauri.conf*.json" -E node_modules . || true
fd -t f "Cargo.toml" -E node_modules . || true
fd -t f -e rs -g "*.rs" . | head -n 200 || true

# Search for plugin-os usage in repo (rust + js)
rg -n "tauri_plugin_os::init\\(\\)|tauri-plugin-os|plugin-os|tauri_plugin_os" -S . || true

# Confirm dependency presence in any package.json
rg -n "\"`@tauri-apps/plugin-os`\"" -S . || true

Repository: AOSSIE-Org/PictoPy

Length of output: 1928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- frontend/src-tauri/Cargo.toml (relevant parts) ----"
sed -n '1,220p' frontend/src-tauri/Cargo.toml

echo "---- frontend/src-tauri/src/main.rs (first 200 lines) ----"
sed -n '1,220p' frontend/src-tauri/src/main.rs

echo "---- frontend/src-tauri/src/lib.rs (first 220 lines) ----"
sed -n '1,220p' frontend/src-tauri/src/lib.rs

echo "---- tauri plugins module search ----"
rg -n "tauri_plugin_os::init\\(|plugin_os::|plugin-os|tauri_plugin_os" -S frontend/src-tauri/src frontend/src-tauri/Cargo.toml || true

echo "---- tauri.conf.json (first 200 lines) ----"
sed -n '1,220p' frontend/src-tauri/tauri.conf.json

Repository: AOSSIE-Org/PictoPy

Length of output: 9812


Replace navigator.platform with Tauri OS platform()

  • navigator.platform is unreliable for OS detection; switch to Tauri’s OS API via @tauri-apps/plugin-os (platform()).
  • This repo isn’t currently wired for plugin-os (no @tauri-apps/plugin-os in frontend/package.json, and no tauri-plugin-os init in frontend/src-tauri/src/main.rs / dependency in frontend/src-tauri/Cargo.toml), so the frontend change requires adding and registering that plugin first.
Suggested change in `frontend/src/utils/serverUtils.ts`
+import { platform } from '`@tauri-apps/plugin-os`';
-const isWindows = () => navigator.platform.startsWith('Win');
+const isWindows = () => platform() === 'windows';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` at line 4, Replace the unreliable
navigator.platform check in isWindows with Tauri’s OS API: add the
`@tauri-apps/plugin-os` dependency to the frontend package manifest and add the
corresponding tauri-plugin-os dependency and plugin registration in the Rust
side (Cargo.toml and tauri main registration) so the plugin is available at
runtime; then update serverUtils.ts to import platform() from
'`@tauri-apps/plugin-os`' and make isWindows an async function that awaits
platform() and returns a boolean when the returned platform string indicates
Windows (e.g., equals or startsWith 'windows').

Comment thread frontend/src/utils/serverUtils.ts Outdated
Comment on lines +6 to +19
const isServerRunning = async (): Promise<boolean> => {
try {
const response = await fetch(BACKEND_URL + '/health');
if (response.ok) {
console.log('Server is Running!');
return true;
} else {
return false;
}
} catch (error) {
console.error('Error checking server status:', error);
return false;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit type annotations per TypeScript guidelines.

The function is missing explicit type annotations for response and the error parameter. Per coding guidelines, TypeScript code should avoid implicit any types and use explicit types.

As per coding guidelines: "Avoid 'any', use explicit types."

🏷️ Proposed fix with explicit types
 const isServerRunning = async (): Promise<boolean> => {
   try {
-    const response = await fetch(BACKEND_URL + '/health');
+    const response: Response = await fetch(BACKEND_URL + '/health');
     if (response.ok) {
       console.log('Server is Running!');
       return true;
     } else {
       return false;
     }
-  } catch (error) {
+  } catch (error: unknown) {
     console.error('Error checking server status:', error);
     return false;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 6 - 19, The isServerRunning
function lacks explicit TypeScript types for the fetched response and the caught
error; update the signature to keep Promise<boolean> and add explicit types
inside the function by annotating response as Response (const response: Response
= await fetch(...)) and typing the catch parameter as unknown (catch (error:
unknown)), then narrow the unknown before logging (e.g., if (error instanceof
Error) use error.message else String(error)) so there are no implicit any types
while preserving the existing control flow.

Comment thread frontend/src/utils/serverUtils.ts Outdated

const isServerRunning = async (): Promise<boolean> => {
try {
const response = await fetch(BACKEND_URL + '/health');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeout to fetch requests to prevent indefinite hanging.

The fetch call lacks a timeout, which means it could hang indefinitely if the backend service is unresponsive or network issues occur. This can leave the startup process in a stuck state.

⏱️ Proposed fix with AbortController timeout
 const isServerRunning = async (): Promise<boolean> => {
   try {
-    const response = await fetch(BACKEND_URL + '/health');
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    const response = await fetch(BACKEND_URL + '/health', { 
+      signal: controller.signal 
+    });
+    clearTimeout(timeoutId);
     if (response.ok) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(BACKEND_URL + '/health');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(BACKEND_URL + '/health', {
signal: controller.signal
});
clearTimeout(timeoutId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` at line 8, The fetch call that assigns
const response = await fetch(BACKEND_URL + '/health') needs a timeout to avoid
hanging; wrap the request with an AbortController, create a timer (e.g., via
setTimeout) that calls controller.abort() after the desired timeout, pass
controller.signal to fetch, and clear the timer (clearTimeout) after fetch
completes; also handle the abort case (AbortError) or rejected promise so the
caller receives a clear timeout error instead of an indefinite hang.

Comment thread frontend/src/utils/serverUtils.ts Outdated
Comment on lines +21 to +34
const isSyncServiceRunning = async (): Promise<boolean> => {
try {
const response = await fetch(SYNC_MICROSERVICE_URL + '/health');
if (response.ok) {
console.log('Sync Service is Running!');
return true;
} else {
return false;
}
} catch (error) {
console.error('Error checking sync service status:', error);
return false;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Eliminate code duplication between health check functions.

The isSyncServiceRunning function is nearly identical to isServerRunning, differing only in the URL and log messages. This violates the DRY (Don't Repeat Yourself) principle and makes maintenance harder.

♻️ Proposed refactor with generic health check
+const checkServiceHealth = async (
+  serviceUrl: string, 
+  serviceName: string
+): Promise<boolean> => {
+  try {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    const response: Response = await fetch(serviceUrl + '/health', { 
+      signal: controller.signal 
+    });
+    clearTimeout(timeoutId);
+    if (response.ok) {
+      console.log(`${serviceName} is Running!`);
+    }
+    return response.ok;
+  } catch (error: unknown) {
+    console.error(`Error checking ${serviceName} status:`, error);
+    return false;
+  }
+};
+
-const isServerRunning = async (): Promise<boolean> => {
-  try {
-    const response = await fetch(BACKEND_URL + '/health');
-    if (response.ok) {
-      console.log('Server is Running!');
-      return true;
-    } else {
-      return false;
-    }
-  } catch (error) {
-    console.error('Error checking server status:', error);
-    return false;
-  }
-};
+const isServerRunning = async (): Promise<boolean> => 
+  checkServiceHealth(BACKEND_URL, 'Server');

-const isSyncServiceRunning = async (): Promise<boolean> => {
-  try {
-    const response = await fetch(SYNC_MICROSERVICE_URL + '/health');
-    if (response.ok) {
-      console.log('Sync Service is Running!');
-      return true;
-    } else {
-      return false;
-    }
-  } catch (error) {
-    console.error('Error checking sync service status:', error);
-    return false;
-  }
-};
+const isSyncServiceRunning = async (): Promise<boolean> => 
+  checkServiceHealth(SYNC_MICROSERVICE_URL, 'Sync Service');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 21 - 34, Refactor the
duplicated health-check logic by extracting a reusable helper (e.g.,
checkServiceHealth) that takes the base URL and an optional service name, moves
the try/catch/fetch/response.ok logic out of isSyncServiceRunning and
isServerRunning, and updates those functions to call this helper; ensure
checkServiceHealth returns a boolean, logs a meaningful message using the
provided service name, and rethrows or handles errors consistently so both
isSyncServiceRunning and isServerRunning simply forward the boolean result.

Comment thread frontend/src/utils/serverUtils.ts Outdated
}
};

export const startServer = async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit return type annotation.

The exported startServer function should have an explicit return type annotation. Per TypeScript guidelines, all public APIs should use explicit types.

As per coding guidelines: "Avoid 'any', use explicit types."

🏷️ Proposed fix
-export const startServer = async () => {
+export const startServer = async (): Promise<void> => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const startServer = async () => {
export const startServer = async (): Promise<void> => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` at line 36, The exported function
startServer lacks an explicit return type; update its signature to include a
concrete TypeScript return type (e.g., Promise<void> if it doesn't return a
value, or Promise<http.Server> / Promise<YourServerType> if it returns a server
instance) instead of using any or leaving it implicit—locate the startServer
declaration and add the appropriate Promise<...> annotation that matches its
implementation.

Comment thread frontend/src/utils/serverUtils.ts Outdated
Comment on lines +36 to +76
export const startServer = async () => {
try {
console.log('Starting services!');

const resourcesFolderPath: string = await invoke(
'get_resources_folder_path',
);

// Start backend server
if (!(await isServerRunning())) {
const backendCommand = Command.create(
isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
'',
{ cwd: resourcesFolderPath + '/backend' },
);

const backendChild = await backendCommand.spawn();
backendCommand.stderr.on('data', (line) =>
console.error('Backend Error:', line),
);
console.log('Backend server started with PID:', backendChild.pid);
}

// Start sync service
if (!(await isSyncServiceRunning())) {
const syncCommand = Command.create(
isWindows() ? 'StartSyncServiceWindows' : 'StartSyncServiceUnix',
'',
{ cwd: resourcesFolderPath + '/sync-microservice' },
);

const syncChild = await syncCommand.spawn();
syncCommand.stderr.on('data', (line) =>
console.error('Sync Service Error:', line),
);
console.log('Sync service started with PID:', syncChild.pid);
}
} catch (error) {
console.error('Error starting services:', error);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add cleanup logic for partial startup failures.

If the backend service starts successfully but the sync service fails to start, there's no cleanup or rollback. This could leave the system in an inconsistent state with only one service running. Consider implementing error recovery to stop any services that did start if the full orchestration fails.

🛡️ Suggested error recovery approach
export const startServer = async (): Promise<void> => {
  const startedServices: { name: string; pid: number }[] = [];
  
  try {
    console.log('Starting services!');
    
    const resourcesFolderPath: string = await invoke('get_resources_folder_path');
    
    // Start backend server
    if (!(await isServerRunning())) {
      const backendChild = await /* spawn logic */;
      startedServices.push({ name: 'backend', pid: backendChild.pid });
      // Verify startup...
    }
    
    // Start sync service
    if (!(await isSyncServiceRunning())) {
      const syncChild = await /* spawn logic */;
      startedServices.push({ name: 'sync', pid: syncChild.pid });
      // Verify startup...
    }
  } catch (error) {
    console.error('Error starting services:', error);
    
    // Cleanup: attempt to stop any services that were started
    for (const service of startedServices) {
      try {
        console.log(`Stopping ${service.name} (PID: ${service.pid})`);
        // Use Tauri's kill functionality or send shutdown signal
      } catch (cleanupError) {
        console.error(`Failed to stop ${service.name}:`, cleanupError);
      }
    }
    
    throw error; // Propagate error to caller
  }
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 36 - 76, startServer
currently can leave the backend running if the sync service fails; add
cleanup/rollback so partial startups are torn down on error: inside startServer
track started services (e.g., push {name:'backend', pid: backendChild.pid} when
backendCommand.spawn() succeeds and similarly for syncChild), wrap orchestration
in try/catch, and on catch iterate the startedServices to gracefully stop/kill
each (using the same Command instance or Tauri kill/stop mechanism), log any
cleanup errors, and rethrow the original error; reference startServer,
isServerRunning, isSyncServiceRunning, backendCommand, syncCommand, backendChild
and syncChild to locate where to add the startedServices array and cleanup code.

Comment thread frontend/src/utils/serverUtils.ts Outdated
Comment on lines +45 to +57
if (!(await isServerRunning())) {
const backendCommand = Command.create(
isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
'',
{ cwd: resourcesFolderPath + '/backend' },
);

const backendChild = await backendCommand.spawn();
backendCommand.stderr.on('data', (line) =>
console.error('Backend Error:', line),
);
console.log('Backend server started with PID:', backendChild.pid);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Verify services started successfully after spawning.

After spawning processes, there's no verification that they actually started successfully and are healthy. The code logs the PID but doesn't wait for the services to become ready or check their health endpoints. This could lead to race conditions where subsequent code tries to use services that haven't fully initialized.

🔍 Suggested verification approach
     const backendChild = await backendCommand.spawn();
     backendCommand.stderr.on('data', (line) =>
       console.error('Backend Error:', line),
     );
     console.log('Backend server started with PID:', backendChild.pid);
+    
+    // Wait for backend to become healthy
+    const maxRetries = 30; // 15 seconds with 500ms intervals
+    for (let i = 0; i < maxRetries; i++) {
+      await new Promise(resolve => setTimeout(resolve, 500));
+      if (await isServerRunning()) {
+        console.log('Backend server is ready');
+        break;
+      }
+      if (i === maxRetries - 1) {
+        throw new Error('Backend server failed to start within timeout');
+      }
+    }
   }

Apply similar logic for the sync service.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!(await isServerRunning())) {
const backendCommand = Command.create(
isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
'',
{ cwd: resourcesFolderPath + '/backend' },
);
const backendChild = await backendCommand.spawn();
backendCommand.stderr.on('data', (line) =>
console.error('Backend Error:', line),
);
console.log('Backend server started with PID:', backendChild.pid);
}
if (!(await isServerRunning())) {
const backendCommand = Command.create(
isWindows() ? 'StartBackendWindows' : 'StartBackendUnix',
'',
{ cwd: resourcesFolderPath + '/backend' },
);
const backendChild = await backendCommand.spawn();
backendCommand.stderr.on('data', (line) =>
console.error('Backend Error:', line),
);
console.log('Backend server started with PID:', backendChild.pid);
// Wait for backend to become healthy
const maxRetries = 30; // 15 seconds with 500ms intervals
for (let i = 0; i < maxRetries; i++) {
await new Promise(resolve => setTimeout(resolve, 500));
if (await isServerRunning()) {
console.log('Backend server is ready');
break;
}
if (i === maxRetries - 1) {
throw new Error('Backend server failed to start within timeout');
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/serverUtils.ts` around lines 45 - 57, After spawning the
backend via Command.create(...) and backendCommand.spawn(), add a readiness
check that polls isServerRunning() (or the backend health endpoint) with a
timeout and retry interval before proceeding; if readiness fails, kill the
spawned process (backendChild.pid) and surface an error via
backendCommand.stderr/error logging. Apply the same polling/timeout pattern for
the sync service; use the existing symbols isServerRunning, Command.create,
backendCommand.spawn, backendCommand.stderr, backendChild.pid,
resourcesFolderPath and the StartBackendWindows/StartBackendUnix commands to
locate where to insert the verification and cleanup logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: face data persists after source folder is removed

2 participants