Improve sync activity UI and registry sync order

This commit is contained in:
Jamie Pine 2025-11-25 09:09:15 -08:00
parent 7a71718eda
commit 48e5c6665a
8 changed files with 93 additions and 29 deletions

View File

@ -767,8 +767,8 @@ pub enum ApplyError {
/// ```
pub async fn compute_registry_sync_order() -> Result<Vec<String>, super::DependencyError> {
use crate::infra::db::entities::{
audit_log, collection, collection_entry, content_identity, device, entry, location, tag,
tag_relationship, user_metadata, user_metadata_tag, volume,
audit_log, collection, collection_entry, content_identity, device, entry, location, space,
space_group, space_item, tag, tag_relationship, user_metadata, user_metadata_tag, volume,
};
// Build iterator of (model_name, dependencies)
@ -809,6 +809,15 @@ pub async fn compute_registry_sync_order() -> Result<Vec<String>, super::Depende
audit_log::Model::SYNC_MODEL,
audit_log::Model::sync_depends_on(),
),
(space::Model::SYNC_MODEL, space::Model::sync_depends_on()),
(
space_group::Model::SYNC_MODEL,
space_group::Model::sync_depends_on(),
),
(
space_item::Model::SYNC_MODEL,
space_item::Model::sync_depends_on(),
),
];
super::dependency_graph::compute_sync_order(models.into_iter())

View File

@ -40,30 +40,73 @@ impl LibraryQuery for GetSyncActivity {
let sync_service = library
.sync_service()
.ok_or_else(|| QueryError::Internal("Sync service not available".to_string()))?;
// Get actual current state from peer sync (not metrics, which might lag)
let current_state = sync_service.peer_sync().state().await;
let metrics = sync_service.metrics();
// Create a snapshot of current metrics
let snapshot = SyncMetricsSnapshot::from_metrics(metrics.metrics()).await;
// Transform into activity summary
let peers: Vec<PeerActivity> = snapshot
.data_volume
.entries_by_device
// Get paired/connected devices from network layer
let network = context.get_networking().await;
let (paired_devices, connected_device_ids) = if let Some(net) = network.as_ref() {
// Get paired devices (need to keep Arc alive and clone the result)
let paired = {
let registry_arc = net.device_registry();
let registry = registry_arc.read().await;
registry.get_paired_devices()
};
// Get connected devices
let connected = net.get_connected_devices().await;
let connected_ids: std::collections::HashSet<_> = connected.into_iter().map(|d| d.device_id).collect();
(paired, connected_ids)
} else {
(Vec::new(), std::collections::HashSet::new())
};
// Build peer list from paired devices, enriched with metrics data and connection status
let peers: Vec<PeerActivity> = paired_devices
.into_iter()
.map(|(device_id, device_metrics)| PeerActivity {
device_id,
device_name: device_metrics.device_name,
is_online: device_metrics.is_online,
last_seen: device_metrics.last_seen,
entries_received: device_metrics.entries_received,
bytes_received: snapshot.data_volume.bytes_received,
bytes_sent: snapshot.data_volume.bytes_sent,
watermark_lag_ms: snapshot.performance.watermark_lag_ms.get(&device_id).copied(),
.map(|device_info| {
// Try to get metrics for this device
let device_metrics = snapshot
.data_volume
.entries_by_device
.get(&device_info.device_id);
// Check if device is actually connected at network level
let is_online = connected_device_ids.contains(&device_info.device_id);
PeerActivity {
device_id: device_info.device_id,
device_name: device_info.device_name.clone(),
is_online,
last_seen: device_metrics
.map(|m| m.last_seen)
.unwrap_or_else(|| chrono::Utc::now()),
entries_received: device_metrics
.map(|m| m.entries_received)
.unwrap_or(0),
bytes_received: device_metrics
.map(|_| snapshot.data_volume.bytes_received)
.unwrap_or(0),
bytes_sent: device_metrics
.map(|_| snapshot.data_volume.bytes_sent)
.unwrap_or(0),
watermark_lag_ms: snapshot
.performance
.watermark_lag_ms
.get(&device_info.device_id)
.copied(),
}
})
.collect();
Ok(GetSyncActivityOutput {
current_state: snapshot.state.current_state,
current_state,
peers,
error_count: snapshot.errors.total_errors,
})

View File

@ -10,6 +10,7 @@ use crate::service::sync::state::DeviceSyncState;
/// Sync activity summary for the UI
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct GetSyncActivityOutput {
pub current_state: DeviceSyncState,
pub peers: Vec<PeerActivity>,
@ -18,6 +19,7 @@ pub struct GetSyncActivityOutput {
/// Per-peer activity information
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct PeerActivity {
pub device_id: Uuid,
pub device_name: String,

View File

@ -20,6 +20,7 @@ import { LocationsSection } from "./components/LocationsSection";
import { Section } from "./components/Section";
import { SidebarItem } from "./components/SidebarItem";
import { JobManagerPopover } from "../JobManager";
import { SyncMonitorPopover } from "../SyncMonitor";
export function Sidebar() {
const client = useSpacedriveClient();

View File

@ -11,6 +11,7 @@ import { useSpacedriveClient } from "../../context";
import { useLibraries } from "../../hooks/useLibraries";
import { usePlatform } from "../../platform";
import { JobManagerPopover } from "../JobManager/JobManagerPopover";
import { SyncMonitorPopover } from "../SyncMonitor";
import clsx from "clsx";
export function SpacesSidebar() {
@ -103,8 +104,9 @@ export function SpacesSidebar() {
{currentSpace && <AddGroupButton spaceId={currentSpace.id} />}
</div>
{/* Job Manager & Settings (pinned to bottom) */}
{/* Sync Monitor, Job Manager & Settings (pinned to bottom) */}
<div className="space-y-0.5">
<SyncMonitorPopover />
<JobManagerPopover />
<button
onClick={() => navigate("/settings")}

View File

@ -7,8 +7,8 @@ import {
Circle,
} from '@phosphor-icons/react';
import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import type { SyncActivity } from '../types';
import { timeAgo } from '../utils';
interface ActivityFeedProps {
activities: SyncActivity[];
@ -81,7 +81,7 @@ function ActivityItem({ activity }: { activity: SyncActivity }) {
<div className="flex-1 min-w-0">
<p className="text-sm text-ink truncate">{activity.description}</p>
<p className="text-xs text-ink-faint">
{formatDistanceToNow(new Date(activity.timestamp), { addSuffix: true })}
{timeAgo(activity.timestamp)}
</p>
</div>
</div>

View File

@ -1,7 +1,7 @@
import { Circle, Lightning } from '@phosphor-icons/react';
import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import type { SyncPeerActivity, SyncState } from '../types';
import { timeAgo } from '../utils';
interface PeerListProps {
peers: SyncPeerActivity[];
@ -59,7 +59,7 @@ function PeerCard({ peer }: { peer: SyncPeerActivity }) {
<span>
{peer.isOnline
? 'Online'
: `Last seen ${formatDistanceToNow(new Date(peer.lastSeen), { addSuffix: true })}`}
: `Last seen ${timeAgo(peer.lastSeen)}`}
</span>
</div>

View File

@ -34,14 +34,21 @@ export function useSyncMonitor() {
useEffect(() => {
if (data) {
const stateValue = data.currentState;
const normalizedState: SyncState =
typeof stateValue === 'string'
? (stateValue as SyncState)
: 'Backfilling' in stateValue
? 'Backfilling'
: 'CatchingUp' in stateValue
? 'CatchingUp'
: 'Uninitialized';
let normalizedState: SyncState;
if (typeof stateValue === 'string') {
normalizedState = stateValue as SyncState;
} else if (typeof stateValue === 'object' && stateValue !== null) {
if ('Backfilling' in stateValue) {
normalizedState = 'Backfilling';
} else if ('CatchingUp' in stateValue) {
normalizedState = 'CatchingUp';
} else {
normalizedState = 'Uninitialized';
}
} else {
normalizedState = 'Uninitialized';
}
setState((prev) => ({
...prev,