@peers-app/peers-ui 0.21.2 → 0.21.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/screens/network-viewer/connection-troubleshooter.js +1 -1
- package/dist/screens/network-viewer/device-details-modal.js +11 -15
- package/dist/screens/network-viewer/group-details-modal.js +60 -17
- package/dist/screens/network-viewer/network-viewer-ipc.d.ts +9 -39
- package/dist/screens/network-viewer/network-viewer-ipc.js +71 -3
- package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +1 -0
- package/dist/screens/network-viewer/network-viewer-ipc.test.js +59 -0
- package/dist/screens/network-viewer/network-viewer.js +46 -60
- package/dist/screens/network-viewer/remote-logs-panel.js +11 -12
- package/dist/tabs-layout/network-status-indicator.d.ts +19 -0
- package/dist/tabs-layout/network-status-indicator.js +105 -0
- package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
- package/dist/tabs-layout/network-status-indicator.test.js +81 -0
- package/dist/tabs-layout/tabs-layout.js +25 -24
- package/dist/tabs-layout/use-network-status.d.ts +8 -0
- package/dist/tabs-layout/use-network-status.js +57 -0
- package/dist/tabs-layout/use-network-status.test.d.ts +1 -0
- package/dist/tabs-layout/use-network-status.test.js +42 -0
- package/package.json +3 -3
- package/src/screens/network-viewer/connection-troubleshooter.tsx +2 -2
- package/src/screens/network-viewer/device-details-modal.tsx +19 -61
- package/src/screens/network-viewer/group-details-modal.tsx +211 -130
- package/src/screens/network-viewer/network-viewer-ipc.test.ts +70 -0
- package/src/screens/network-viewer/network-viewer-ipc.ts +102 -35
- package/src/screens/network-viewer/network-viewer.tsx +76 -148
- package/src/screens/network-viewer/remote-logs-panel.tsx +14 -16
- package/src/tabs-layout/network-status-indicator.test.tsx +94 -0
- package/src/tabs-layout/network-status-indicator.tsx +209 -0
- package/src/tabs-layout/tabs-layout.tsx +4 -1
- package/src/tabs-layout/use-network-status.test.tsx +55 -0
- package/src/tabs-layout/use-network-status.ts +68 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
4
|
+
const react_1 = require("@testing-library/react");
|
|
5
|
+
const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
|
|
6
|
+
const use_network_status_1 = require("./use-network-status");
|
|
7
|
+
jest.mock("../screens/network-viewer/network-viewer-ipc", () => ({
|
|
8
|
+
getNetworkDiagnosticsConsumer: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
const mockGetDiagnostics = network_viewer_ipc_1.getNetworkDiagnosticsConsumer;
|
|
11
|
+
function makeStatus(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
state: "offline",
|
|
14
|
+
connectionCount: 0,
|
|
15
|
+
syncingContextCount: 0,
|
|
16
|
+
errorCount: 0,
|
|
17
|
+
errors: [],
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe("useNetworkStatus", () => {
|
|
22
|
+
it("streams status updates and reports host disconnection", async () => {
|
|
23
|
+
const status = (0, peers_sdk_1.observable)(makeStatus());
|
|
24
|
+
mockGetDiagnostics.mockResolvedValue({
|
|
25
|
+
observables: { status },
|
|
26
|
+
});
|
|
27
|
+
const [transport] = (0, peers_sdk_1.inProcessTransportPair)();
|
|
28
|
+
const disposeTransport = (0, peers_sdk_1.registerHostContractTransport)(transport);
|
|
29
|
+
const hook = (0, react_1.renderHook)(() => (0, use_network_status_1.useNetworkStatus)());
|
|
30
|
+
await (0, react_1.waitFor)(() => expect(hook.result.current.availability).toBe("ready"));
|
|
31
|
+
expect(hook.result.current.status?.state).toBe("offline");
|
|
32
|
+
(0, react_1.act)(() => {
|
|
33
|
+
status(makeStatus({ state: "syncing", connectionCount: 2, syncingContextCount: 1 }));
|
|
34
|
+
});
|
|
35
|
+
await (0, react_1.waitFor)(() => expect(hook.result.current.status?.state).toBe("syncing"));
|
|
36
|
+
(0, react_1.act)(() => {
|
|
37
|
+
disposeTransport();
|
|
38
|
+
});
|
|
39
|
+
await (0, react_1.waitFor)(() => expect(hook.result.current.availability).toBe("unavailable"));
|
|
40
|
+
hook.unmount();
|
|
41
|
+
});
|
|
42
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "biome check --write ."
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@peers-app/peers-sdk": "^0.21.
|
|
31
|
+
"@peers-app/peers-sdk": "^0.21.3",
|
|
32
32
|
"bootstrap": "^5.3.3",
|
|
33
33
|
"react": "^18.0.0",
|
|
34
34
|
"react-dom": "^18.0.0"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@babel/preset-react": "^7.24.1",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.1",
|
|
41
41
|
"@biomejs/biome": "2.3.11",
|
|
42
|
-
"@peers-app/peers-sdk": "0.21.
|
|
42
|
+
"@peers-app/peers-sdk": "0.21.3",
|
|
43
43
|
"@testing-library/dom": "^10.4.0",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
45
|
"@testing-library/react": "^16.3.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { getNetworkViewerPlatformApi } from "./network-viewer-ipc";
|
|
4
4
|
|
|
5
5
|
export interface IDiagnosticCheck {
|
|
6
6
|
id: string;
|
|
@@ -106,7 +106,7 @@ export function ConnectionTroubleshooter({ onBack }: Props) {
|
|
|
106
106
|
setChecks([]);
|
|
107
107
|
setExpanded(new Set());
|
|
108
108
|
|
|
109
|
-
const api =
|
|
109
|
+
const api = getNetworkViewerPlatformApi();
|
|
110
110
|
if (!api) {
|
|
111
111
|
setChecks([
|
|
112
112
|
{
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { TrustLevel } from "@peers-app/peers-sdk";
|
|
1
|
+
import { type INetworkDeviceDetails, TrustLevel } from "@peers-app/peers-sdk";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
4
4
|
import { TrustLevelBadge } from "../../components/trust-level-badge";
|
|
5
|
-
import {
|
|
5
|
+
import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
|
|
6
6
|
import { RemoteLogsPanel } from "./remote-logs-panel";
|
|
7
7
|
|
|
8
8
|
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
@@ -24,41 +24,6 @@ function formatRate(mbps: number | undefined): string {
|
|
|
24
24
|
return `${mbps.toFixed(2)} MB/s`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// TypeScript type definitions (matching backend)
|
|
28
|
-
interface IDeviceDetails {
|
|
29
|
-
deviceId: string;
|
|
30
|
-
userId: string;
|
|
31
|
-
userName?: string;
|
|
32
|
-
deviceName?: string;
|
|
33
|
-
trustLevel: number;
|
|
34
|
-
connectionState: "connected" | "syncing" | "idle";
|
|
35
|
-
canReadRemoteLogs: boolean;
|
|
36
|
-
sharedGroups: string[];
|
|
37
|
-
roles: Record<string, unknown>;
|
|
38
|
-
pathType: "direct" | "indirect" | "unknown";
|
|
39
|
-
throughDeviceIds?: string[];
|
|
40
|
-
networkInfo?: {
|
|
41
|
-
deviceId: string;
|
|
42
|
-
connections: Array<{ deviceId: string; userId: string }>;
|
|
43
|
-
preferredDeviceIds: string[];
|
|
44
|
-
connectionSlotsAvailable: number;
|
|
45
|
-
cpuPercent: number;
|
|
46
|
-
memPercent: number;
|
|
47
|
-
timestampLastApplied: number;
|
|
48
|
-
};
|
|
49
|
-
syncStatsByGroup: {
|
|
50
|
-
[groupId: string]: {
|
|
51
|
-
lastSyncTime: number;
|
|
52
|
-
isPreferred: boolean;
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
// Throughput stats
|
|
56
|
-
bytesSent?: number;
|
|
57
|
-
bytesReceived?: number;
|
|
58
|
-
sendRateMBps?: number;
|
|
59
|
-
receiveRateMBps?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
27
|
interface IDeviceDetailsModalProps {
|
|
63
28
|
deviceId: string;
|
|
64
29
|
onClose: () => void;
|
|
@@ -67,22 +32,14 @@ interface IDeviceDetailsModalProps {
|
|
|
67
32
|
|
|
68
33
|
/** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
|
|
69
34
|
export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceDetailsModalProps) {
|
|
70
|
-
const [device, setDevice] = useState<
|
|
35
|
+
const [device, setDevice] = useState<INetworkDeviceDetails | null>(null);
|
|
71
36
|
const [loading, setLoading] = useState(true);
|
|
72
37
|
const [notFound, setNotFound] = useState(false);
|
|
73
38
|
|
|
74
39
|
const loadData = useCallback(async () => {
|
|
75
40
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
console.warn("Network Viewer API not available");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const deviceData = (await api.getDeviceDetails(deviceId)) as
|
|
83
|
-
| IDeviceDetails
|
|
84
|
-
| null
|
|
85
|
-
| undefined;
|
|
41
|
+
const diagnostics = await getNetworkDiagnosticsConsumer();
|
|
42
|
+
const { device: deviceData } = await diagnostics.tools.getDeviceDetails({ deviceId });
|
|
86
43
|
|
|
87
44
|
if (!deviceData) {
|
|
88
45
|
setNotFound(true);
|
|
@@ -104,14 +61,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
104
61
|
}
|
|
105
62
|
|
|
106
63
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
onClose();
|
|
64
|
+
const control = await getNetworkControlConsumer();
|
|
65
|
+
const result = await control.tools.disconnectDevice({ deviceId });
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
alert(result.message);
|
|
68
|
+
return;
|
|
114
69
|
}
|
|
70
|
+
onDisconnect?.();
|
|
71
|
+
onClose();
|
|
115
72
|
} catch (error) {
|
|
116
73
|
console.error("Error disconnecting device:", error);
|
|
117
74
|
}
|
|
@@ -197,9 +154,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
197
154
|
<span className="badge bg-success">{device.connectionState}</span>
|
|
198
155
|
</div>
|
|
199
156
|
<div className="col-md-6 mb-3">
|
|
200
|
-
<strong>Trust
|
|
157
|
+
<strong>User Trust:</strong>
|
|
201
158
|
<br />
|
|
202
|
-
<TrustLevelBadge level={device.
|
|
159
|
+
<TrustLevelBadge level={device.userTrustLevel || TrustLevel.Unknown} />
|
|
160
|
+
</div>
|
|
161
|
+
<div className="col-md-6 mb-3">
|
|
162
|
+
<strong>Device Trust:</strong>
|
|
163
|
+
<br />
|
|
164
|
+
<TrustLevelBadge level={device.deviceTrustLevel || TrustLevel.Unknown} />
|
|
203
165
|
</div>
|
|
204
166
|
<div className="col-md-6 mb-3">
|
|
205
167
|
<strong>Path Type:</strong>
|
|
@@ -345,7 +307,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
345
307
|
<thead>
|
|
346
308
|
<tr>
|
|
347
309
|
<th>Device ID</th>
|
|
348
|
-
<th>User ID</th>
|
|
349
310
|
</tr>
|
|
350
311
|
</thead>
|
|
351
312
|
<tbody>
|
|
@@ -354,9 +315,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
354
315
|
<td>
|
|
355
316
|
<code className="text-muted small">{conn.deviceId}</code>
|
|
356
317
|
</td>
|
|
357
|
-
<td>
|
|
358
|
-
<code className="text-muted small">{conn.userId}</code>
|
|
359
|
-
</td>
|
|
360
318
|
</tr>
|
|
361
319
|
))}
|
|
362
320
|
</tbody>
|
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type INetworkGroupDetails,
|
|
3
|
+
type INetworkGroupSyncInfo,
|
|
4
|
+
TrustLevel,
|
|
5
|
+
} from "@peers-app/peers-sdk";
|
|
2
6
|
import { useCallback, useEffect, useState } from "react";
|
|
3
7
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
4
8
|
import { TrustLevelBadge } from "../../components/trust-level-badge";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
groupName: string;
|
|
11
|
-
founderUserId: string;
|
|
12
|
-
founderUserName?: string;
|
|
13
|
-
members: {
|
|
14
|
-
userId: string;
|
|
15
|
-
userName?: string;
|
|
16
|
-
role: string;
|
|
17
|
-
trustLevel: TrustLevel;
|
|
18
|
-
}[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface IConnectedDevice {
|
|
22
|
-
deviceId: string;
|
|
23
|
-
latencyMs: number;
|
|
24
|
-
errorRate: number;
|
|
25
|
-
timestampLastApplied: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface IGroupSyncInfo {
|
|
29
|
-
groupId: string;
|
|
30
|
-
groupName: string;
|
|
31
|
-
connectedDevices: IConnectedDevice[];
|
|
32
|
-
preferredDeviceIds: string[];
|
|
33
|
-
lastSyncTimestamp: number;
|
|
34
|
-
totalMembers: number;
|
|
35
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
getNetworkControlConsumer,
|
|
11
|
+
getNetworkDiagnosticsConsumer,
|
|
12
|
+
getNetworkViewerPlatformApi,
|
|
13
|
+
} from "./network-viewer-ipc";
|
|
36
14
|
|
|
37
15
|
interface IGroupDetailsModalProps {
|
|
38
16
|
groupId: string;
|
|
@@ -40,8 +18,8 @@ interface IGroupDetailsModalProps {
|
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps) {
|
|
43
|
-
const [group, setGroup] = useState<
|
|
44
|
-
const [syncInfo, setSyncInfo] = useState<
|
|
21
|
+
const [group, setGroup] = useState<INetworkGroupDetails | null>(null);
|
|
22
|
+
const [syncInfo, setSyncInfo] = useState<INetworkGroupSyncInfo | null>(null);
|
|
45
23
|
const [loading, setLoading] = useState(true);
|
|
46
24
|
const [notFound, setNotFound] = useState(false);
|
|
47
25
|
const [downloading, setDownloading] = useState(false);
|
|
@@ -52,21 +30,26 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
52
30
|
filePath?: string;
|
|
53
31
|
size?: number;
|
|
54
32
|
} | null>(null);
|
|
33
|
+
const [fullSyncing, setFullSyncing] = useState(false);
|
|
34
|
+
const [fullSyncResult, setFullSyncResult] = useState<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
message: string;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
const [syncingNow, setSyncingNow] = useState(false);
|
|
39
|
+
const [syncNowResult, setSyncNowResult] = useState<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
message: string;
|
|
42
|
+
} | null>(null);
|
|
43
|
+
const platformApi = getNetworkViewerPlatformApi();
|
|
55
44
|
|
|
56
45
|
const loadData = useCallback(async () => {
|
|
57
46
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const [groupData, syncDataUnknown] = await Promise.all([
|
|
65
|
-
api.getGroupDetails(groupId),
|
|
66
|
-
api.getGroupSyncStatus(groupId),
|
|
47
|
+
const diagnostics = await getNetworkDiagnosticsConsumer();
|
|
48
|
+
const [{ group: groupDetails }, { groups }] = await Promise.all([
|
|
49
|
+
diagnostics.tools.getGroupDetails({ groupId }),
|
|
50
|
+
diagnostics.tools.getGroupSyncStatus({ groupId }),
|
|
67
51
|
]);
|
|
68
52
|
|
|
69
|
-
const groupDetails = groupData as IGroupDetails | null | undefined;
|
|
70
53
|
if (!groupDetails) {
|
|
71
54
|
setNotFound(true);
|
|
72
55
|
} else {
|
|
@@ -74,9 +57,8 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
74
57
|
setNotFound(false);
|
|
75
58
|
}
|
|
76
59
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
setSyncInfo(syncData[0]);
|
|
60
|
+
if (groups.length > 0) {
|
|
61
|
+
setSyncInfo(groups[0]);
|
|
80
62
|
}
|
|
81
63
|
} catch (error) {
|
|
82
64
|
console.error("Error loading group details:", error);
|
|
@@ -92,8 +74,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
92
74
|
setDownloadProgress("Preparing download...");
|
|
93
75
|
setDownloadResult(null);
|
|
94
76
|
|
|
95
|
-
|
|
96
|
-
if (!api?.downloadRemoteDatabase) {
|
|
77
|
+
if (!platformApi?.downloadRemoteDatabase) {
|
|
97
78
|
setDownloadResult({
|
|
98
79
|
success: false,
|
|
99
80
|
message: "Download API not available",
|
|
@@ -102,7 +83,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
102
83
|
}
|
|
103
84
|
|
|
104
85
|
setDownloadProgress("Downloading database...");
|
|
105
|
-
const result = (await
|
|
86
|
+
const result = (await platformApi.downloadRemoteDatabase(groupId, deviceId)) as {
|
|
106
87
|
success: boolean;
|
|
107
88
|
message: string;
|
|
108
89
|
filePath?: string;
|
|
@@ -125,6 +106,53 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
125
106
|
}
|
|
126
107
|
};
|
|
127
108
|
|
|
109
|
+
const handleSyncNow = async () => {
|
|
110
|
+
setSyncingNow(true);
|
|
111
|
+
setSyncNowResult(null);
|
|
112
|
+
try {
|
|
113
|
+
const control = await getNetworkControlConsumer();
|
|
114
|
+
const result = await control.tools.syncGroupNow({ groupId });
|
|
115
|
+
setSyncNowResult(result);
|
|
116
|
+
if (result.success) {
|
|
117
|
+
await loadData();
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
setSyncNowResult({
|
|
121
|
+
success: false,
|
|
122
|
+
message: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
setSyncingNow(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleFullSync = async () => {
|
|
130
|
+
if (
|
|
131
|
+
!confirm(
|
|
132
|
+
`Do a full sync for "${group?.groupName || groupId}"? This resets local change tracking for only this context and may transfer substantial data.`,
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setFullSyncing(true);
|
|
138
|
+
setFullSyncResult(null);
|
|
139
|
+
try {
|
|
140
|
+
const control = await getNetworkControlConsumer();
|
|
141
|
+
const result = await control.tools.resetChangeTracking({ dataContextId: groupId });
|
|
142
|
+
setFullSyncResult(result);
|
|
143
|
+
if (result.success) {
|
|
144
|
+
await loadData();
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
setFullSyncResult({
|
|
148
|
+
success: false,
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
} finally {
|
|
152
|
+
setFullSyncing(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
128
156
|
useEffect(() => {
|
|
129
157
|
loadData();
|
|
130
158
|
}, [loadData]);
|
|
@@ -239,98 +267,151 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
239
267
|
</div>
|
|
240
268
|
)}
|
|
241
269
|
|
|
242
|
-
{/* Database Download Section */}
|
|
243
270
|
<div className="mb-4">
|
|
244
271
|
<h6 className="border-bottom pb-2 mb-3">
|
|
245
|
-
<i className="bi bi-
|
|
246
|
-
|
|
272
|
+
<i className="bi bi-arrow-repeat me-2"></i>
|
|
273
|
+
Synchronization
|
|
247
274
|
</h6>
|
|
248
275
|
<p className="text-muted small mb-3">
|
|
249
|
-
|
|
250
|
-
|
|
276
|
+
<strong>Sync now</strong> notifies connected peers that this context may have
|
|
277
|
+
changes. <strong>Do full sync</strong> resets change tracking for this context and
|
|
278
|
+
rebuilds synchronization state; that may transfer substantial data and should be
|
|
279
|
+
used only to repair suspected drift.
|
|
251
280
|
</p>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{device.deviceId.substring(0, 8)}...
|
|
277
|
-
<span className="badge bg-secondary ms-1">
|
|
278
|
-
{Math.round(device.latencyMs)}ms
|
|
279
|
-
</span>
|
|
280
|
-
</>
|
|
281
|
-
)}
|
|
282
|
-
</button>
|
|
283
|
-
))}
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
) : (
|
|
287
|
-
<div className="alert alert-info mb-0">
|
|
288
|
-
<i className="bi bi-info-circle me-2"></i>
|
|
289
|
-
No devices are currently connected to this group. Connect to a device first to
|
|
290
|
-
download its database.
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
|
|
294
|
-
{downloadProgress && (
|
|
295
|
-
<div className="mt-3">
|
|
296
|
-
<div className="d-flex align-items-center">
|
|
297
|
-
<span
|
|
298
|
-
className="spinner-border spinner-border-sm me-2"
|
|
299
|
-
role="status"
|
|
300
|
-
aria-hidden="true"
|
|
301
|
-
></span>
|
|
302
|
-
<span>{downloadProgress}</span>
|
|
303
|
-
</div>
|
|
281
|
+
<div className="d-flex flex-wrap gap-2">
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
className="btn btn-sm btn-outline-primary"
|
|
285
|
+
onClick={() => void handleSyncNow()}
|
|
286
|
+
disabled={syncingNow || fullSyncing}
|
|
287
|
+
>
|
|
288
|
+
{syncingNow ? "Sending sync notification..." : "Sync now"}
|
|
289
|
+
</button>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
className="btn btn-sm btn-outline-warning"
|
|
293
|
+
onClick={() => void handleFullSync()}
|
|
294
|
+
disabled={fullSyncing || syncingNow}
|
|
295
|
+
>
|
|
296
|
+
{fullSyncing ? "Starting full sync..." : "Do full sync"}
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
{syncNowResult && (
|
|
300
|
+
<div
|
|
301
|
+
className={`alert mt-3 mb-0 ${syncNowResult.success ? "alert-success" : "alert-danger"}`}
|
|
302
|
+
role="status"
|
|
303
|
+
>
|
|
304
|
+
{syncNowResult.message}
|
|
304
305
|
</div>
|
|
305
306
|
)}
|
|
306
|
-
|
|
307
|
-
{downloadResult && (
|
|
307
|
+
{fullSyncResult && (
|
|
308
308
|
<div
|
|
309
|
-
className={`alert mt-3 mb-0 ${
|
|
309
|
+
className={`alert mt-3 mb-0 ${fullSyncResult.success ? "alert-success" : "alert-danger"}`}
|
|
310
|
+
role="status"
|
|
310
311
|
>
|
|
311
|
-
{
|
|
312
|
-
<>
|
|
313
|
-
<i className="bi bi-check-circle me-2"></i>
|
|
314
|
-
<strong>Download complete!</strong>
|
|
315
|
-
<br />
|
|
316
|
-
<small>
|
|
317
|
-
File: <code>{downloadResult.filePath}</code>
|
|
318
|
-
<br />
|
|
319
|
-
Size:{" "}
|
|
320
|
-
{downloadResult.size
|
|
321
|
-
? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
|
|
322
|
-
: "Unknown"}
|
|
323
|
-
</small>
|
|
324
|
-
</>
|
|
325
|
-
) : (
|
|
326
|
-
<>
|
|
327
|
-
<i className="bi bi-exclamation-circle me-2"></i>
|
|
328
|
-
<strong>Download failed:</strong> {downloadResult.message}
|
|
329
|
-
</>
|
|
330
|
-
)}
|
|
312
|
+
{fullSyncResult.message}
|
|
331
313
|
</div>
|
|
332
314
|
)}
|
|
333
315
|
</div>
|
|
316
|
+
|
|
317
|
+
{/* Database Download Section */}
|
|
318
|
+
{platformApi?.downloadRemoteDatabase ? (
|
|
319
|
+
<div className="mb-4">
|
|
320
|
+
<h6 className="border-bottom pb-2 mb-3">
|
|
321
|
+
<i className="bi bi-download me-2"></i>
|
|
322
|
+
Download Database (Fast Sync)
|
|
323
|
+
</h6>
|
|
324
|
+
<p className="text-muted small mb-3">
|
|
325
|
+
Download the entire database from a connected device. This is useful for fast
|
|
326
|
+
initial sync on a new device. The database will be saved as a backup file.
|
|
327
|
+
</p>
|
|
328
|
+
|
|
329
|
+
{syncInfo && syncInfo.connectedDevices.length > 0 ? (
|
|
330
|
+
<div>
|
|
331
|
+
<label className="form-label">Select a connected device:</label>
|
|
332
|
+
<div className="d-flex flex-wrap gap-2">
|
|
333
|
+
{syncInfo.connectedDevices.map((device) => (
|
|
334
|
+
<button
|
|
335
|
+
key={device.deviceId}
|
|
336
|
+
className="btn btn-outline-primary btn-sm"
|
|
337
|
+
onClick={() => handleDownloadDatabase(device.deviceId)}
|
|
338
|
+
disabled={downloading}
|
|
339
|
+
>
|
|
340
|
+
{downloading ? (
|
|
341
|
+
<>
|
|
342
|
+
<span
|
|
343
|
+
className="spinner-border spinner-border-sm me-1"
|
|
344
|
+
role="status"
|
|
345
|
+
aria-hidden="true"
|
|
346
|
+
></span>
|
|
347
|
+
Downloading...
|
|
348
|
+
</>
|
|
349
|
+
) : (
|
|
350
|
+
<>
|
|
351
|
+
<i className="bi bi-cloud-download me-1"></i>
|
|
352
|
+
{device.deviceId.substring(0, 8)}...
|
|
353
|
+
<span className="badge bg-secondary ms-1">
|
|
354
|
+
{Math.round(device.latencyMs)}ms
|
|
355
|
+
</span>
|
|
356
|
+
</>
|
|
357
|
+
)}
|
|
358
|
+
</button>
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
) : (
|
|
363
|
+
<div className="alert alert-info mb-0">
|
|
364
|
+
<i className="bi bi-info-circle me-2"></i>
|
|
365
|
+
No devices are currently connected to this group. Connect to a device first to
|
|
366
|
+
download its database.
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{downloadProgress && (
|
|
371
|
+
<div className="mt-3">
|
|
372
|
+
<div className="d-flex align-items-center">
|
|
373
|
+
<span
|
|
374
|
+
className="spinner-border spinner-border-sm me-2"
|
|
375
|
+
role="status"
|
|
376
|
+
aria-hidden="true"
|
|
377
|
+
></span>
|
|
378
|
+
<span>{downloadProgress}</span>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{downloadResult && (
|
|
384
|
+
<div
|
|
385
|
+
className={`alert mt-3 mb-0 ${downloadResult.success ? "alert-success" : "alert-danger"}`}
|
|
386
|
+
>
|
|
387
|
+
{downloadResult.success ? (
|
|
388
|
+
<>
|
|
389
|
+
<i className="bi bi-check-circle me-2"></i>
|
|
390
|
+
<strong>Download complete!</strong>
|
|
391
|
+
<br />
|
|
392
|
+
<small>
|
|
393
|
+
File: <code>{downloadResult.filePath}</code>
|
|
394
|
+
<br />
|
|
395
|
+
Size:{" "}
|
|
396
|
+
{downloadResult.size
|
|
397
|
+
? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
|
|
398
|
+
: "Unknown"}
|
|
399
|
+
</small>
|
|
400
|
+
</>
|
|
401
|
+
) : (
|
|
402
|
+
<>
|
|
403
|
+
<i className="bi bi-exclamation-circle me-2"></i>
|
|
404
|
+
<strong>Download failed:</strong> {downloadResult.message}
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
) : (
|
|
411
|
+
<div className="alert alert-info mb-4">
|
|
412
|
+
Raw database download is available only in the Electron desktop app.
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
334
415
|
</>
|
|
335
416
|
)}
|
|
336
417
|
</div>
|