Fixed face count inconsistency after folder removal #573
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughBackend 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. ChangesSystem Cleanup and Infrastructure Updates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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)
| 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]); |
There was a problem hiding this comment.
🛠️ 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:
-
Remove the console.log on line 30 before merging to production. Logging filtered data is helpful during development but should not ship to users.
-
The local variable
clusterson line 25 shadows the Redux state variable from line 16, making the code harder to follow. Consider renaming the local variable toallClustersorfetchedClusters. -
The
|| []fallback on line 25 is redundant since line 24 already ensuresclustersData?.data?.clustersexists 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.
| 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.
|
@rahulharpal1603 sir please review this |
|
|
||
| const filteredClusters = clusters.filter((c) => c.face_count > 0); | ||
|
|
||
| dispatch(setClusters(filteredClusters)); | ||
| // console.log(filteredClusters); |
There was a problem hiding this comment.
This is not the correct way. You need to ensure that empty face clusters are deleted at the database level.
There was a problem hiding this comment.
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 ??
There was a problem hiding this comment.
ok sir i will write a route to delete all face id with 0 face count ??
There was a problem hiding this comment.
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:
Line 25: The local variable
clustersshadows the Redux state variableclustersfrom line 16, reducing code clarity.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
📒 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:
Line 1081-1083: The
input_typeparameter now uses a direct$refwith sibling properties (valid in OpenAPI 3.1), but this seems unrelated to the face clustering fix.Line 2158: Adding
"additionalProperties": truetoImageInCluster.Metadatabroadens 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
There was a problem hiding this comment.
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
clustersshadows the Redux state variable from line 16, making the code harder to follow and maintain. Consider renaming tofetchedClustersorallClusters.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
📒 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 oncluster_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 afterdb_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.
| 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() |
There was a problem hiding this comment.
Optimize the subquery and add error handling.
The current implementation has two issues:
-
Performance concern: The
NOT INsubquery can be slow with large datasets because it doesn't leverage indexes efficiently. With many clusters and faces, this could block the database. -
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.
| 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() |
There was a problem hiding this comment.
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:
- Delayed response to the client
- Potential timeout on slow databases
- 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.
| 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() |
There was a problem hiding this comment.
🛠️ 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:
- Architecture violation: Utils layer should not contain raw SQL queries; database operations should be encapsulated in the database layer.
- Code duplication: Identical SQL in two places makes maintenance harder and increases risk of divergence.
- 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.
| 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.
…to fix/face_inconsistency_after_folder_removal
|
@rahulharpal1603 sir can u please review it |
There was a problem hiding this comment.
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
stopServerthat 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 anasynclifespan blocks the event loop; switch toawait asyncio.sleep()and usetime.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_Serverand./PictoPy_Syncare 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.exeor-Commandon 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: 60with a fixedretryDelay: 1000means 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 usesresources/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.
| - repo: https://github.com/astral-sh/ruff-pre-commit | ||
| rev: v0.4.10 |
There was a problem hiding this comment.
🧩 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-commitis v0.14.14 (Jan 22, 2026). [1] - The repository README currently recommends using
rev: v0.14.14in.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-commitv0.4.10 is from 2024; the latest release is v0.14.14 (Jan 22, 2026). Updaterevtov0.14.14.black24.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.
| 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) |
There was a problem hiding this comment.
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.
| // 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); |
There was a problem hiding this comment.
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.
| // 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.
| } 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); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's see the full context of setup.js to understand the change
cat -n scripts/setup.jsRepository: 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 -200Repository: AOSSIE-Org/PictoPy
Length of output: 2156
🏁 Script executed:
# Search for macOS/Darwin references in documentation
rg -i 'macos|darwin|osx' --type=mdRepository: 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 -50Repository: 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 -5Repository: 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.
| # ---- 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 .. |
There was a problem hiding this comment.
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 1Note: 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.
| # ---- 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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
| host="localhost", | ||
| port=52124, |
There was a problem hiding this comment.
🧩 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(usually127.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.0binds 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 bind0.0.0.0inside 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-imageSources: [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.
|
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: |
There was a problem hiding this comment.
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 winRemove 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_modelsshould 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 liftConsider moving cluster cleanup to background processing.
This synchronous call to
cluster_util_delete_empty_clusters()is inconsistent with the pattern used inpost_folder_add_sequence(line 84) andpost_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 valueSimplify 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 valueConsider attaching stdout listener for complete logging.
Only
stderris being monitored, but important startup messages and logs might be written tostdout. 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 winConsider 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 winUse 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
📒 Files selected for processing (5)
app.pybackend/app/routes/folders.pydocs/backend/backend_python/openapi.jsonfrontend/src/components/Media/MediaView.tsxfrontend/src/utils/serverUtils.ts
| def initialize_app(): | ||
| # ...existing code... | ||
| # Clear any stale cache data on startup | ||
| invalidate_cache() | ||
| # ...existing code... |
There was a problem hiding this comment.
🧩 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.pyRepository: 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 || trueRepository: 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 || trueRepository: 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.
| 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); | ||
| // } | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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'); |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/tauri-apps/tauri/blob/73a0ad4/tooling/api/src/os.ts
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform
🏁 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 . || trueRepository: 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 . || trueRepository: 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:
- 1: https://v2.tauri.app/reference/javascript/os/
- 2: https://v2.tauri.app/reference/javascript/api/
- 3: https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/os
🌐 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:
- 1: https://v2.tauri.app/reference/javascript/os/
- 2: https://github.com/tauri-apps/tauri-plugin-os/blob/v2/dist-js/index.d.ts
- 3: https://v2.tauri.app/plugin/os-info/
- 4: https://tauri.app/v1/api/js/os
🏁 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 || trueRepository: 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 . || trueRepository: 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.jsonRepository: AOSSIE-Org/PictoPy
Length of output: 9812
Replace navigator.platform with Tauri OS platform()
navigator.platformis 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-osinfrontend/package.json, and notauri-plugin-osinit infrontend/src-tauri/src/main.rs/ dependency infrontend/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').
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
|
|
||
| const isServerRunning = async (): Promise<boolean> => { | ||
| try { | ||
| const response = await fetch(BACKEND_URL + '/health'); |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| } | ||
| }; | ||
|
|
||
| export const startServer = async () => { |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
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
Improvements