mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
Improve sync activity UI and registry sync order
This commit is contained in:
parent
7a71718eda
commit
48e5c6665a
@ -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())
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user