Skip to content

Conversation

@nicotsx
Copy link
Owner

@nicotsx nicotsx commented Feb 11, 2026

Closes #474 #128

Summary by CodeRabbit

  • New Features

    • Added real-time mirror synchronization progress indicators with animated visual feedback.
    • Mirrors now display "in progress" status during copy operations.
    • Repository details page now includes breadcrumb navigation.
  • Improvements

    • Enhanced form behavior when changing notification types and backend selections with targeted field resets.
    • Improved snapshot selection control for better user intent alignment.

refactor: remove unnecessary useEffects
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Walkthrough

This PR adds "in_progress" status support for mirror synchronization operations. It expands status types across server and client layers, implements real-time server event handling in the UI to reflect ongoing mirror operations, and refactors form reset logic from useEffect dependencies to event handlers.

Changes

Cohort / File(s) Summary
Mirror Status Type System
app/server/db/schema.ts, app/server/modules/backups/backups.dto.ts, app/server/modules/backups/backups.queries.ts
Expanded mirror copy status union type from "success" | "error" to "in_progress" | "success" | "error" across database schema, DTOs, and query layer. Refactored mirrorQueries.updateStatus to accept optional fields via new MirrorStatusUpdate type.
Client API & Event Types
app/client/api-client/types.gen.ts, app/client/hooks/use-server-events.ts
Extended lastCopyStatus in API response types and MirrorEvent.status to include "in_progress" option for real-time synchronization state propagation.
Mirror Execution Lifecycle
app/server/modules/backups/backups.execution.ts
Added preprocessing step in copyToSingleMirror to set mirror copy status to in_progress and clear lastCopyError before initiating copy operation.
Mirror Status UI Component
app/client/modules/backups/components/schedule-mirrors-config.tsx
Implemented real-time server event subscription (mirror:started, mirror:completed) to update mirror statuses live; refactored assignment construction to map-based approach; added isSyncing state, animated status indicator, and dynamic label generation for synchronization progress display.
Form Reset Refactoring
app/client/modules/notifications/components/create-notification-form.tsx, app/client/modules/repositories/components/create-repository-form.tsx, app/client/modules/volumes/components/create-volume-form.tsx
Migrated form reset logic from useEffect dependencies to inline onValueChange handlers on type/backend selectors, eliminating automatic re-initialization on field changes and enabling targeted field preservation during resets.
Navigation & Selection Changes
app/client/modules/backups/routes/backups.tsx, app/client/modules/repositories/routes/repository-details.tsx, app/client/modules/backups/components/snapshot-timeline.tsx
Simplified backup reordering state management with nullable localItems; added exported handle property for breadcrumb generation in repository details; removed automatic snapshot selection on mount in snapshot timeline.

Possibly related PRs

  • fix(mirrors): keep last copy state #125: Modifies mirror "last copy" metadata handling—preserves fields across mirror updates while this PR expands and propagates "in_progress" status and lifecycle updates.
  • feat: mirror repositories #95: Introduces mirror repositories feature that directly intersects with this PR's additions to mirror copy status, server events, DB schema, and client types.
  • refactor: split backups module + unit tests #463: Extends mirror/backup status handling with "in_progress" state across backups-related modules (schema, DTOs, execution, queries) that were refactored in a related PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several refactoring changes (useEffect removals in form components, snapshot timeline auto-selection removal, backup reordering logic, repository breadcrumb export) are out of scope to issue #474 and appear unrelated to mirror progress indication. Isolate mirror progress feature changes into a separate PR; move unrelated refactoring (form effects, snapshot timeline, backup reordering, breadcrumb changes) to distinct PRs focused on those specific improvements.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature implemented: adding a progress indicator for mirrors, which aligns with the core changes across UI components, server logic, and type definitions.
Linked Issues check ✅ Passed The PR implements the feature requirements from issue #474: expanding lastCopyStatus to support 'in_progress' state, adding server event handling for mirror:started/mirror:completed events, and displaying real-time progress feedback with animated status indicators in the UI.

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


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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
app/client/modules/backups/components/schedule-mirrors-config.tsx (1)

68-72: ⚠️ Potential issue | 🟡 Minor

Potential race: React Query refetch may overwrite SSE-driven in_progress state.

When the SSE sets lastCopyStatus to "in_progress", a subsequent React Query refetch of currentMirrors (e.g., on window focus) could overwrite the assignment map with stale server data that doesn't yet reflect in_progress, causing the progress indicator to briefly disappear.

One approach would be to merge rather than replace — e.g., skip overwriting entries currently marked in_progress unless the server data also shows in_progress or a terminal status. That said, this is a timing edge case and unlikely to be disruptive in practice.

🤖 Fix all issues with AI agents
In `@app/client/modules/backups/components/schedule-mirrors-config.tsx`:
- Around line 355-360: The StatusDot is currently left to its internal default
when not syncing which causes it to animate for terminal variants; change the
prop so animation only occurs while syncing by explicitly passing a boolean:
compute animated via isSyncing(assignment) (e.g.,
animated={isSyncing(assignment)}) or check both syncing and variant from
getStatusVariant(assignment.lastCopyStatus) so that success/error terminal
variants get animated=false and only the syncing state sets animated=true;
update the StatusDot usage in schedule-mirrors-config.tsx accordingly.
- Around line 101-135: The SSE subscription is reinitialized because
addEventListener (from useServerEvents) is not memoized and changes each render;
update the useServerEvents hook to wrap and return addEventListener with
useCallback so it has a stable identity, ensure any internal dependencies are
listed in that useCallback dependency array, and then keep the current useEffect
in schedule-mirrors-config.tsx using addEventListener and scheduleId as
dependencies so subscriptions only change when truly needed.

In `@app/client/modules/backups/routes/backups.tsx`:
- Around line 50-54: The drag-reorder can pass -1 to arrayMove when active.id or
over.id aren’t found in baseItems; inside the setLocalItems updater (symbols:
setLocalItems, baseItems, arrayMove, active.id, over.id) check for missing IDs
and avoid calling arrayMove with -1: compute oldIndex/newIndex, and if either is
-1 then either rebuild baseItems from schedules?.map(s=>s.id) and recompute
indices, or if still missing simply return currentItems to noop the update;
ensure you use the same updater return type so no other state changes occur.
🧹 Nitpick comments (5)
app/client/modules/volumes/components/create-volume-form.tsx (1)

127-138: Good refactor: moving form reset from useEffect to the event handler.

This is a cleaner pattern that avoids unnecessary render cycles and makes the reset logic explicit and traceable.

One minor note: in create mode, field.onChange(value) on Line 129 is immediately overwritten by form.reset(...) on Line 131. It's harmless but redundant for that branch. Consider moving it into an else:

Optional: avoid redundant field.onChange in create mode
 onValueChange={(value) => {
-  field.onChange(value);
   if (mode === "create") {
     form.reset({
       name: form.getValues().name,
       ...defaultValuesForType[value as keyof typeof defaultValuesForType],
     });
+  } else {
+    field.onChange(value);
   }
 }}
app/client/modules/repositories/components/create-repository-form.tsx (1)

120-131: Nit: field.onChange(value) on Line 122 is redundant.

form.reset(...) on Line 123 already sets the backend field via the defaultValuesForType spread, so the explicit field.onChange(value) call is overwritten immediately. Removing it would reduce confusion about which call actually takes effect.

Suggested diff
 							<Select
 								onValueChange={(value) => {
-									field.onChange(value);
 									form.reset({
 										name: form.getValues().name,
 										isExistingRepository: form.getValues().isExistingRepository,
 										customPassword: form.getValues().customPassword,
 										...defaultValuesForType[value as keyof typeof defaultValuesForType],
 									});
 								}}
 								value={field.value}
 							>
app/client/modules/backups/routes/backups.tsx (1)

30-31: localItems is never reset, causing stale UI after server-side data changes.

Once the user drags (setting localItems), the derived items list will always use localItems and never reflect server-side changes (e.g., newly created or deleted schedules) until the component remounts. Consider resetting localItems to null when schedules data changes, so the component falls back to the server-derived order.

Suggested approach

One option is to use a key or reset mechanism. A simple approach:

+import { useState, useEffect } from "react";
 ...
 const [localItems, setLocalItems] = useState<number[] | null>(null);
 const items = localItems ?? schedules?.map((s) => s.id) ?? [];

+// Reset local drag state when server data changes
+useEffect(() => {
+	setLocalItems(null);
+}, [schedules]);

Alternatively, you could key the component on a derived value from schedules (e.g., length or a hash of IDs) to force a remount when the set of schedules changes.

app/server/modules/backups/backups.execution.ts (1)

369-436: Consider recovery for mirrors stuck in in_progress after a crash.

If the process crashes between setting in_progress (line 379) and the catch/success handlers, the mirror will remain stuck in in_progress indefinitely. The backup schedule has a recovery mechanism (per learnings from PR #463 — the stopBackup function resets stuck states), but mirrors lack an equivalent.

This could be addressed in a follow-up with a startup sweep that resets any mirrors still in in_progress to "error", similar to the pattern used for backup schedules. Based on learnings, the stopBackup function intentionally updates the schedule status in a finally block even when no backup is running, as a recovery mechanism to reset schedules stuck in "in_progress" state when backups crash unexpectedly.

app/client/modules/backups/components/schedule-mirrors-config.tsx (1)

207-212: Consider using the MirrorAssignment["lastCopyStatus"] type instead of string | null.

This would provide compile-time safety ensuring only valid status values are passed and the function stays in sync with MirrorAssignment.

Suggested change
-const getStatusVariant = (status: string | null) => {
+const getStatusVariant = (status: MirrorAssignment["lastCopyStatus"]) => {

Comment on lines +101 to +135
useEffect(() => {
const unsubscribeStarted = addEventListener("mirror:started", (data) => {
const event = data as { scheduleId: number; repositoryId: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, { ...existing, lastCopyStatus: "in_progress", lastCopyError: null });
return next;
});
});

const unsubscribeCompleted = addEventListener("mirror:completed", (data) => {
const event = data as { scheduleId: number; repositoryId: string; status?: "success" | "error"; error?: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, {
...existing,
lastCopyStatus: event.status ?? existing.lastCopyStatus,
lastCopyError: event.error ?? null,
lastCopyAt: Date.now(),
});
return next;
});
});

return () => {
unsubscribeStarted();
unsubscribeCompleted();
};
}, [addEventListener, scheduleId]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "use-server-events.ts" --exec cat {}

Repository: nicotsx/zerobyte

Length of output: 6190


addEventListener is not memoized, causing the SSE subscription to reinitialize on every render.

The addEventListener function returned from useServerEvents() is created fresh on every render without useCallback wrapping. Including it in the dependency array at line 133 ([addEventListener, scheduleId]) causes the useEffect to re-run on every component render, unsubscribing and resubscribing to events each time. This creates a brief window where handlers are inactive, risking missed events during the re-subscription cycle.

Wrap addEventListener in useCallback within the useServerEvents hook to provide a stable reference across renders.

🤖 Prompt for AI Agents
In `@app/client/modules/backups/components/schedule-mirrors-config.tsx` around
lines 101 - 135, The SSE subscription is reinitialized because addEventListener
(from useServerEvents) is not memoized and changes each render; update the
useServerEvents hook to wrap and return addEventListener with useCallback so it
has a stable identity, ensure any internal dependencies are listed in that
useCallback dependency array, and then keep the current useEffect in
schedule-mirrors-config.tsx using addEventListener and scheduleId as
dependencies so subscriptions only change when truly needed.

Comment on lines +355 to +360
<div className="flex items-center gap-2">
<StatusDot
variant={getStatusVariant(assignment.lastCopyStatus)}
label={getStatusLabel(assignment)}
animated={isSyncing(assignment) ? true : undefined}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

StatusDot will animate for success and error variants by default — likely unintended.

When isSyncing is false, animated is passed as undefined. Looking at StatusDot, animated ?? true is the default for success, error, and info variants, so the dot will keep pinging even after sync completes. You probably want a static dot for terminal states and animation only during syncing.

Suggested fix
 <StatusDot
   variant={getStatusVariant(assignment.lastCopyStatus)}
   label={getStatusLabel(assignment)}
-  animated={isSyncing(assignment) ? true : undefined}
+  animated={isSyncing(assignment)}
 />
📝 Committable suggestion

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

Suggested change
<div className="flex items-center gap-2">
<StatusDot
variant={getStatusVariant(assignment.lastCopyStatus)}
label={getStatusLabel(assignment)}
animated={isSyncing(assignment) ? true : undefined}
/>
<div className="flex items-center gap-2">
<StatusDot
variant={getStatusVariant(assignment.lastCopyStatus)}
label={getStatusLabel(assignment)}
animated={isSyncing(assignment)}
/>
🤖 Prompt for AI Agents
In `@app/client/modules/backups/components/schedule-mirrors-config.tsx` around
lines 355 - 360, The StatusDot is currently left to its internal default when
not syncing which causes it to animate for terminal variants; change the prop so
animation only occurs while syncing by explicitly passing a boolean: compute
animated via isSyncing(assignment) (e.g., animated={isSyncing(assignment)}) or
check both syncing and variant from getStatusVariant(assignment.lastCopyStatus)
so that success/error terminal variants get animated=false and only the syncing
state sets animated=true; update the StatusDot usage in
schedule-mirrors-config.tsx accordingly.

Comment on lines +50 to +54
setLocalItems((currentItems) => {
const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
const oldIndex = baseItems.indexOf(active.id as number);
const newIndex = baseItems.indexOf(over.id as number);
const newItems = arrayMove(baseItems, oldIndex, newIndex);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

indexOf returns -1 if the dragged ID is missing from baseItems, producing unexpected behavior in arrayMove.

If baseItems is derived from the stale localItems (which could happen after schedules change server-side), active.id or over.id might not be found, resulting in oldIndex or newIndex being -1. arrayMove with -1 would produce an incorrect reordering.

Suggested guard
 setLocalItems((currentItems) => {
 	const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
 	const oldIndex = baseItems.indexOf(active.id as number);
 	const newIndex = baseItems.indexOf(over.id as number);
+	if (oldIndex === -1 || newIndex === -1) return currentItems;
 	const newItems = arrayMove(baseItems, oldIndex, newIndex);
 	reorderMutation.mutate({ body: { scheduleIds: newItems } });
 	return newItems;
 });
📝 Committable suggestion

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

Suggested change
setLocalItems((currentItems) => {
const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
const oldIndex = baseItems.indexOf(active.id as number);
const newIndex = baseItems.indexOf(over.id as number);
const newItems = arrayMove(baseItems, oldIndex, newIndex);
setLocalItems((currentItems) => {
const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
const oldIndex = baseItems.indexOf(active.id as number);
const newIndex = baseItems.indexOf(over.id as number);
if (oldIndex === -1 || newIndex === -1) return currentItems;
const newItems = arrayMove(baseItems, oldIndex, newIndex);
🤖 Prompt for AI Agents
In `@app/client/modules/backups/routes/backups.tsx` around lines 50 - 54, The
drag-reorder can pass -1 to arrayMove when active.id or over.id aren’t found in
baseItems; inside the setLocalItems updater (symbols: setLocalItems, baseItems,
arrayMove, active.id, over.id) check for missing IDs and avoid calling arrayMove
with -1: compute oldIndex/newIndex, and if either is -1 then either rebuild
baseItems from schedules?.map(s=>s.id) and recompute indices, or if still
missing simply return currentItems to noop the update; ensure you use the same
updater return type so no other state changes occur.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add progress indicator/status for mirrored repository synchronization

1 participant