@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.
Files changed (31) hide show
  1. package/dist/screens/network-viewer/connection-troubleshooter.js +1 -1
  2. package/dist/screens/network-viewer/device-details-modal.js +11 -15
  3. package/dist/screens/network-viewer/group-details-modal.js +60 -17
  4. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +9 -39
  5. package/dist/screens/network-viewer/network-viewer-ipc.js +71 -3
  6. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +1 -0
  7. package/dist/screens/network-viewer/network-viewer-ipc.test.js +59 -0
  8. package/dist/screens/network-viewer/network-viewer.js +46 -60
  9. package/dist/screens/network-viewer/remote-logs-panel.js +11 -12
  10. package/dist/tabs-layout/network-status-indicator.d.ts +19 -0
  11. package/dist/tabs-layout/network-status-indicator.js +105 -0
  12. package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
  13. package/dist/tabs-layout/network-status-indicator.test.js +81 -0
  14. package/dist/tabs-layout/tabs-layout.js +25 -24
  15. package/dist/tabs-layout/use-network-status.d.ts +8 -0
  16. package/dist/tabs-layout/use-network-status.js +57 -0
  17. package/dist/tabs-layout/use-network-status.test.d.ts +1 -0
  18. package/dist/tabs-layout/use-network-status.test.js +42 -0
  19. package/package.json +3 -3
  20. package/src/screens/network-viewer/connection-troubleshooter.tsx +2 -2
  21. package/src/screens/network-viewer/device-details-modal.tsx +19 -61
  22. package/src/screens/network-viewer/group-details-modal.tsx +211 -130
  23. package/src/screens/network-viewer/network-viewer-ipc.test.ts +70 -0
  24. package/src/screens/network-viewer/network-viewer-ipc.ts +102 -35
  25. package/src/screens/network-viewer/network-viewer.tsx +76 -148
  26. package/src/screens/network-viewer/remote-logs-panel.tsx +14 -16
  27. package/src/tabs-layout/network-status-indicator.test.tsx +94 -0
  28. package/src/tabs-layout/network-status-indicator.tsx +209 -0
  29. package/src/tabs-layout/tabs-layout.tsx +4 -1
  30. package/src/tabs-layout/use-network-status.test.tsx +55 -0
  31. package/src/tabs-layout/use-network-status.ts +68 -0
@@ -0,0 +1,70 @@
1
+ import {
2
+ CONTRACT_REQUEST_CHANNEL,
3
+ inProcessTransportPair,
4
+ newid,
5
+ registerHostContractTransport,
6
+ } from "@peers-app/peers-sdk";
7
+ import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
8
+
9
+ function setupHost(label: string) {
10
+ const [consumerTransport, providerTransport] = inProcessTransportPair();
11
+ providerTransport.on(CONTRACT_REQUEST_CHANNEL, (call) => {
12
+ if (call.member === "observable" && call.name === "status" && call.operation === "get") {
13
+ return {
14
+ state: "offline",
15
+ connectionCount: 0,
16
+ syncingContextCount: 0,
17
+ errorCount: 0,
18
+ errors: [],
19
+ };
20
+ }
21
+ if (call.member === "tool" && call.name === "getOverview") {
22
+ return { overview: { localDeviceId: label } };
23
+ }
24
+ if (call.member === "tool" && call.name === "refreshNetworkInfo") {
25
+ return { success: true, message: label };
26
+ }
27
+ return undefined;
28
+ });
29
+ return {
30
+ dispose: registerHostContractTransport(consumerTransport),
31
+ };
32
+ }
33
+
34
+ describe("Network Viewer host consumers", () => {
35
+ it("caches consumers for one transport and recreates them after replacement", async () => {
36
+ const firstHost = setupHost(newid());
37
+ const firstDiagnostics = await getNetworkDiagnosticsConsumer();
38
+ expect(await getNetworkDiagnosticsConsumer()).toBe(firstDiagnostics);
39
+
40
+ const secondLabel = newid();
41
+ const secondHost = setupHost(secondLabel);
42
+ const secondDiagnostics = await getNetworkDiagnosticsConsumer();
43
+ const control = await getNetworkControlConsumer();
44
+
45
+ expect(secondDiagnostics).not.toBe(firstDiagnostics);
46
+ await expect(firstDiagnostics.tools.getOverview()).rejects.toThrow("disposed");
47
+ await expect(control.tools.refreshNetworkInfo()).resolves.toEqual({
48
+ success: true,
49
+ message: secondLabel,
50
+ });
51
+
52
+ secondHost.dispose();
53
+ firstHost.dispose();
54
+ });
55
+
56
+ it("shares one consumer pair across concurrent callers", async () => {
57
+ const host = setupHost(newid());
58
+ const [first, second, third] = await Promise.all([
59
+ getNetworkDiagnosticsConsumer(),
60
+ getNetworkDiagnosticsConsumer(),
61
+ getNetworkControlConsumer(),
62
+ ]);
63
+
64
+ expect(second).toBe(first);
65
+ const cachedControl = await getNetworkControlConsumer();
66
+ expect(third).toBe(cachedControl);
67
+
68
+ host.dispose();
69
+ });
70
+ });
@@ -1,46 +1,113 @@
1
- import type { IConsoleLog } from "@peers-app/peers-sdk";
2
-
3
- /** Filters and bounds accepted by the desktop remote-log diagnostic endpoint. */
4
- export type RemoteLogsQuery = {
5
- sinceTimestamp?: number;
6
- level?: IConsoleLog["level"];
7
- process?: string;
8
- search?: string;
9
- pageSize?: number;
10
- timeoutMs?: number;
11
- };
1
+ import {
2
+ createContractConsumer,
3
+ getHostContractTransport,
4
+ getNetworkControlContractDefinition,
5
+ getNetworkDiagnosticsContractDefinition,
6
+ type IContractTransport,
7
+ type INetworkControlConsumer,
8
+ type INetworkDiagnosticsConsumer,
9
+ subscribeHostContractTransport,
10
+ waitForHostContractTransport,
11
+ } from "@peers-app/peers-sdk";
12
12
 
13
- /** Result of reading a directly connected, provider-authorized device's console-log table. */
14
- export type RemoteLogsResult = {
15
- deviceId: string;
16
- truncated: boolean;
17
- logs: IConsoleLog[];
18
- queriedAt: number;
19
- };
13
+ /** Default wait for the host transport before treating diagnostics as unavailable. */
14
+ const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
20
15
 
21
- /** Electron preload APIs used by the Network Viewer screen. */
22
- export type NetworkViewerElectronApi = {
23
- getOverview: () => Promise<unknown>;
24
- getConnections: () => Promise<unknown>;
25
- /** No-arg: all groups; with id: single group. */
26
- getGroupSyncStatus: (groupId?: string) => Promise<unknown>;
27
- getDisconnectedDevices: () => Promise<unknown>;
28
- refreshNetworkInfo: () => Promise<void>;
29
- disconnectDevice: (deviceId: string) => Promise<void>;
30
- connectToDevice: (deviceId: string) => Promise<{ success: boolean; message?: string }>;
31
- deleteDevice: (deviceId: string) => Promise<{ success: boolean; message?: string }>;
32
- getDeviceDetails: (deviceId: string) => Promise<unknown>;
33
- getGroupDetails: (groupId: string) => Promise<unknown>;
34
- getRemoteLogs: (deviceId: string, query?: RemoteLogsQuery) => Promise<RemoteLogsResult>;
16
+ /** Electron-only capabilities intentionally excluded from portable network contracts. */
17
+ export type NetworkViewerPlatformApi = {
35
18
  downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
36
19
  runDiagnostics: () => Promise<unknown>;
37
20
  };
38
21
 
39
22
  type WindowWithNetworkViewer = Window & {
40
- electronAPI?: { networkViewer?: NetworkViewerElectronApi };
23
+ electronAPI?: { networkViewer?: NetworkViewerPlatformApi };
41
24
  };
42
25
 
43
- /** Return the context-isolated desktop diagnostics bridge when running in Electron. */
44
- export function getNetworkViewerApi(): NetworkViewerElectronApi | undefined {
26
+ interface INetworkViewerConsumers {
27
+ transport: IContractTransport;
28
+ diagnostics: INetworkDiagnosticsConsumer;
29
+ control: INetworkControlConsumer;
30
+ }
31
+
32
+ let cachedConsumers: INetworkViewerConsumers | undefined;
33
+ let pendingConsumers: Promise<INetworkViewerConsumers> | undefined;
34
+
35
+ function disposeCachedConsumers(): void {
36
+ cachedConsumers?.diagnostics.dispose();
37
+ cachedConsumers?.control.dispose();
38
+ cachedConsumers = undefined;
39
+ }
40
+
41
+ subscribeHostContractTransport(
42
+ (transport) => {
43
+ if (cachedConsumers && cachedConsumers.transport !== transport) {
44
+ disposeCachedConsumers();
45
+ }
46
+ },
47
+ { emitCurrent: false },
48
+ );
49
+
50
+ async function loadConsumers(transport: IContractTransport): Promise<INetworkViewerConsumers> {
51
+ disposeCachedConsumers();
52
+ const diagnostics = createContractConsumer(
53
+ getNetworkDiagnosticsContractDefinition(),
54
+ transport,
55
+ ) as unknown as INetworkDiagnosticsConsumer;
56
+ const control = createContractConsumer(
57
+ getNetworkControlContractDefinition(),
58
+ transport,
59
+ ) as unknown as INetworkControlConsumer;
60
+ try {
61
+ await diagnostics.loadingPromise;
62
+ if (getHostContractTransport() !== transport) {
63
+ diagnostics.dispose();
64
+ control.dispose();
65
+ // Clear before recursing so getConsumers can start a fresh in-flight memo for the
66
+ // current transport, and so this promise's finally does not wipe that newer memo.
67
+ pendingConsumers = undefined;
68
+ return getConsumers();
69
+ }
70
+ cachedConsumers = { transport, diagnostics, control };
71
+ return cachedConsumers;
72
+ } catch (error) {
73
+ diagnostics.dispose();
74
+ control.dispose();
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ async function getConsumers(): Promise<INetworkViewerConsumers> {
80
+ const transport =
81
+ getHostContractTransport() ??
82
+ (await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
83
+ if (cachedConsumers?.transport === transport) {
84
+ return cachedConsumers;
85
+ }
86
+ if (pendingConsumers) {
87
+ return pendingConsumers;
88
+ }
89
+ const loadPromise = loadConsumers(transport);
90
+ pendingConsumers = loadPromise;
91
+ try {
92
+ return await loadPromise;
93
+ } finally {
94
+ if (pendingConsumers === loadPromise) {
95
+ pendingConsumers = undefined;
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Return a typed diagnostics consumer bound to the current local host transport. */
101
+ export async function getNetworkDiagnosticsConsumer(): Promise<INetworkDiagnosticsConsumer> {
102
+ return (await getConsumers()).diagnostics;
103
+ }
104
+
105
+ /** Return a typed control consumer bound to the current local host transport. */
106
+ export async function getNetworkControlConsumer(): Promise<INetworkControlConsumer> {
107
+ return (await getConsumers()).control;
108
+ }
109
+
110
+ /** Return the context-isolated desktop-only capability bridge, when available. */
111
+ export function getNetworkViewerPlatformApi(): NetworkViewerPlatformApi | undefined {
45
112
  return (window as WindowWithNetworkViewer).electronAPI?.networkViewer;
46
113
  }
@@ -1,13 +1,22 @@
1
- import { TrustLevel } from "@peers-app/peers-sdk";
1
+ import {
2
+ type INetworkConnectionInfo,
3
+ type INetworkDisconnectedDevice,
4
+ type INetworkGroupSyncInfo,
5
+ type INetworkOverview,
6
+ TrustLevel,
7
+ } from "@peers-app/peers-sdk";
2
8
  import { useCallback, useEffect, useState } from "react";
3
9
  import { LoadingIndicator } from "../../components/loading-indicator";
4
10
  import { TrustLevelBadge } from "../../components/trust-level-badge";
5
- import { isDesktop } from "../../globals";
6
11
  import { registerInternalPeersUI } from "../../ui-router/ui-loader";
7
12
  import { ConnectionTroubleshooter } from "./connection-troubleshooter";
8
13
  import { DeviceDetailsModal } from "./device-details-modal";
9
14
  import { GroupDetailsModal } from "./group-details-modal";
10
- import { getNetworkViewerApi } from "./network-viewer-ipc";
15
+ import {
16
+ getNetworkControlConsumer,
17
+ getNetworkDiagnosticsConsumer,
18
+ getNetworkViewerPlatformApi,
19
+ } from "./network-viewer-ipc";
11
20
  import { UsageGraph } from "./usage-graph";
12
21
 
13
22
  /** Format bytes to human-readable string (KB, MB, GB) */
@@ -29,81 +38,11 @@ function formatRate(mbps: number | undefined): string {
29
38
  return `${mbps.toFixed(2)} MB/s`;
30
39
  }
31
40
 
32
- // TypeScript type definitions (matching backend)
33
- interface INetworkOverview {
34
- localDeviceId: string;
35
- localUserId: string;
36
- totalConnections: number;
37
- availableSlots: number;
38
- maxConnections: number;
39
- cpuPercent: number;
40
- memPercent: number;
41
- groups: IGroupSummary[];
42
- }
43
-
44
- interface IGroupSummary {
45
- groupId: string;
46
- groupName: string;
47
- connectedDevices: number;
48
- totalMembers: number;
49
- }
50
-
51
- interface IConnectionInfo {
52
- deviceId: string;
53
- userId: string;
54
- userName?: string;
55
- deviceName?: string;
56
- userTrustLevel: number;
57
- deviceTrustLevel: number;
58
- connectionState: "connected" | "syncing" | "idle";
59
- sharedGroups: string[];
60
- roles: Record<string, unknown>;
61
- pathType: "direct" | "indirect" | "unknown";
62
- throughDeviceIds?: string[];
63
- latencyMs: number;
64
- errorRate: number;
65
- isServer: boolean;
66
- connectionAddress?: string;
67
- // Throughput stats
68
- bytesSent?: number;
69
- bytesReceived?: number;
70
- sendRateMBps?: number;
71
- receiveRateMBps?: number;
72
- }
73
-
74
- interface IDeviceConnection {
75
- deviceId: string;
76
- latencyMs: number;
77
- errorRate: number;
78
- timestampLastApplied: number;
79
- closed?: boolean;
80
- }
81
-
82
- interface IGroupSyncInfo {
83
- groupId: string;
84
- groupName: string;
85
- connectedDevices: IDeviceConnection[];
86
- preferredDeviceIds: string[];
87
- lastSyncTimestamp: number;
88
- totalMembers: number;
89
- }
90
-
91
- interface IDisconnectedDevice {
92
- deviceId: string;
93
- userId: string;
94
- userName?: string;
95
- trustLevel: number;
96
- firstSeen: Date;
97
- lastSeen: Date;
98
- name?: string;
99
- serverUrl?: string;
100
- }
101
-
102
41
  export function NetworkViewerList() {
103
42
  const [overview, setOverview] = useState<INetworkOverview | null>(null);
104
- const [connections, setConnections] = useState<IConnectionInfo[]>([]);
105
- const [groupSyncInfo, setGroupSyncInfo] = useState<IGroupSyncInfo[]>([]);
106
- const [disconnectedDevices, setDisconnectedDevices] = useState<IDisconnectedDevice[]>([]);
43
+ const [connections, setConnections] = useState<INetworkConnectionInfo[]>([]);
44
+ const [groupSyncInfo, setGroupSyncInfo] = useState<INetworkGroupSyncInfo[]>([]);
45
+ const [disconnectedDevices, setDisconnectedDevices] = useState<INetworkDisconnectedDevice[]>([]);
107
46
  const [loading, setLoading] = useState(true);
108
47
  const [refreshing, setRefreshing] = useState(false);
109
48
  const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
@@ -113,24 +52,23 @@ export function NetworkViewerList() {
113
52
 
114
53
  const loadData = useCallback(async () => {
115
54
  try {
116
- if (!isDesktop()) return;
117
- const api = getNetworkViewerApi();
118
- if (!api) {
119
- console.warn("Network Viewer API not available");
120
- return;
121
- }
122
-
123
- const [overviewData, connectionsData, groupSyncData, disconnectedData] = await Promise.all([
124
- api.getOverview(),
125
- api.getConnections(),
126
- api.getGroupSyncStatus(),
127
- api.getDisconnectedDevices(),
55
+ const diagnostics = await getNetworkDiagnosticsConsumer();
56
+ const [
57
+ { overview: overviewData },
58
+ { connections: connectionsData },
59
+ { groups: groupSyncData },
60
+ { devices: disconnectedData },
61
+ ] = await Promise.all([
62
+ diagnostics.tools.getOverview(),
63
+ diagnostics.tools.getConnections(),
64
+ diagnostics.tools.getGroupSyncStatus(),
65
+ diagnostics.tools.getDisconnectedDevices(),
128
66
  ]);
129
67
 
130
- setOverview(overviewData as INetworkOverview);
131
- setConnections(connectionsData as IConnectionInfo[]);
132
- setGroupSyncInfo(groupSyncData as IGroupSyncInfo[]);
133
- setDisconnectedDevices(disconnectedData as IDisconnectedDevice[]);
68
+ setOverview(overviewData);
69
+ setConnections(connectionsData);
70
+ setGroupSyncInfo(groupSyncData);
71
+ setDisconnectedDevices(disconnectedData);
134
72
  } catch (error) {
135
73
  console.error("Error loading network data:", error);
136
74
  } finally {
@@ -141,10 +79,12 @@ export function NetworkViewerList() {
141
79
  const handleRefresh = async () => {
142
80
  setRefreshing(true);
143
81
  try {
144
- const api = getNetworkViewerApi();
145
- if (api) {
146
- await api.refreshNetworkInfo();
82
+ const control = await getNetworkControlConsumer();
83
+ const result = await control.tools.refreshNetworkInfo();
84
+ if (result.success) {
147
85
  await loadData();
86
+ } else {
87
+ alert(result.message);
148
88
  }
149
89
  } catch (error) {
150
90
  console.error("Error refreshing network info:", error);
@@ -159,10 +99,12 @@ export function NetworkViewerList() {
159
99
  }
160
100
 
161
101
  try {
162
- const api = getNetworkViewerApi();
163
- if (api) {
164
- await api.disconnectDevice(deviceId);
102
+ const control = await getNetworkControlConsumer();
103
+ const result = await control.tools.disconnectDevice({ deviceId });
104
+ if (result.success) {
165
105
  await loadData();
106
+ } else {
107
+ alert(result.message);
166
108
  }
167
109
  } catch (error) {
168
110
  console.error("Error disconnecting device:", error);
@@ -175,16 +117,13 @@ export function NetworkViewerList() {
175
117
 
176
118
  const handleConnectToDevice = async (deviceId: string) => {
177
119
  try {
178
- const api = getNetworkViewerApi();
179
- if (api) {
180
- setConnecting((prev) => new Set(prev).add(deviceId));
181
- const result = await api.connectToDevice(deviceId);
182
- if (result.success) {
183
- // Refresh data to see if connection was established
184
- await loadData();
185
- } else {
186
- alert(`Failed to connect: ${result.message}`);
187
- }
120
+ const control = await getNetworkControlConsumer();
121
+ setConnecting((prev) => new Set(prev).add(deviceId));
122
+ const result = await control.tools.connectToDevice({ deviceId });
123
+ if (result.success) {
124
+ await loadData();
125
+ } else {
126
+ alert(`Failed to connect: ${result.message}`);
188
127
  }
189
128
  } catch (error) {
190
129
  console.error("Error connecting to device:", error);
@@ -200,15 +139,12 @@ export function NetworkViewerList() {
200
139
 
201
140
  const handleDeleteDevice = async (deviceId: string) => {
202
141
  try {
203
- const api = getNetworkViewerApi();
204
- if (api) {
205
- const result = await api.deleteDevice(deviceId);
206
- if (result.success) {
207
- // Refresh data to remove the device from the list
208
- await loadData();
209
- } else {
210
- alert(`Failed to delete device: ${result.message}`);
211
- }
142
+ const control = await getNetworkControlConsumer();
143
+ const result = await control.tools.deleteDevice({ deviceId });
144
+ if (result.success) {
145
+ await loadData();
146
+ } else {
147
+ alert(`Failed to delete device: ${result.message}`);
212
148
  }
213
149
  } catch (error) {
214
150
  console.error("Error deleting device:", error);
@@ -227,16 +163,6 @@ export function NetworkViewerList() {
227
163
  return <LoadingIndicator />;
228
164
  }
229
165
 
230
- if (!isDesktop()) {
231
- return (
232
- <div className="container-fluid p-4">
233
- <div className="alert alert-info">
234
- Network Viewer is only available in the desktop application.
235
- </div>
236
- </div>
237
- );
238
- }
239
-
240
166
  if (!overview) {
241
167
  return (
242
168
  <div className="container-fluid p-4">
@@ -254,14 +180,16 @@ export function NetworkViewerList() {
254
180
  <div className="d-flex justify-content-between align-items-center mb-4">
255
181
  <h2>Network Viewer</h2>
256
182
  <div className="d-flex gap-2">
257
- <button
258
- className="btn btn-sm btn-outline-warning"
259
- onClick={() => setShowTroubleshooter(true)}
260
- title="Run connection diagnostics"
261
- >
262
- <i className="bi bi-stethoscope me-1"></i>
263
- Troubleshoot
264
- </button>
183
+ {getNetworkViewerPlatformApi()?.runDiagnostics && (
184
+ <button
185
+ className="btn btn-sm btn-outline-warning"
186
+ onClick={() => setShowTroubleshooter(true)}
187
+ title="Run connection diagnostics"
188
+ >
189
+ <i className="bi bi-stethoscope me-1"></i>
190
+ Troubleshoot
191
+ </button>
192
+ )}
265
193
  <button className="btn btn-sm btn-primary" onClick={handleRefresh} disabled={refreshing}>
266
194
  <i className={`bi bi-arrow-clockwise ${refreshing ? "spin" : ""}`}></i>
267
195
  {refreshing ? " Refreshing..." : " Refresh"}
@@ -294,10 +222,9 @@ export function NetworkViewerList() {
294
222
  <UsageGraph
295
223
  label="Connections"
296
224
  fetchUsage={async () => {
297
- const api = getNetworkViewerApi();
298
- if (!api) return 0;
299
- const data = (await api.getOverview()) as INetworkOverview;
300
- return data.totalConnections;
225
+ const diagnostics = await getNetworkDiagnosticsConsumer();
226
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
227
+ return currentOverview.totalConnections;
301
228
  }}
302
229
  durationMinutes={5}
303
230
  updateInterval={5000}
@@ -321,10 +248,9 @@ export function NetworkViewerList() {
321
248
  <UsageGraph
322
249
  label="CPU"
323
250
  fetchUsage={async () => {
324
- const api = getNetworkViewerApi();
325
- if (!api) return 0;
326
- const data = (await api.getOverview()) as INetworkOverview;
327
- return data.cpuPercent;
251
+ const diagnostics = await getNetworkDiagnosticsConsumer();
252
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
253
+ return currentOverview.cpuPercent;
328
254
  }}
329
255
  durationMinutes={5}
330
256
  updateInterval={5000}
@@ -344,10 +270,9 @@ export function NetworkViewerList() {
344
270
  <UsageGraph
345
271
  label="Memory"
346
272
  fetchUsage={async () => {
347
- const api = getNetworkViewerApi();
348
- if (!api) return 0;
349
- const data = (await api.getOverview()) as INetworkOverview;
350
- return data.memPercent;
273
+ const diagnostics = await getNetworkDiagnosticsConsumer();
274
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
275
+ return currentOverview.memPercent;
351
276
  }}
352
277
  durationMinutes={5}
353
278
  updateInterval={5000}
@@ -578,7 +503,10 @@ export function NetworkViewerList() {
578
503
  {(device.errorRate * 100).toFixed(1)}% errors
579
504
  </span>
580
505
  <span className="badge bg-info">
581
- Last: {new Date(device.timestampLastApplied).toLocaleString()}
506
+ Last:{" "}
507
+ {device.timestampLastApplied
508
+ ? new Date(device.timestampLastApplied).toLocaleString()
509
+ : "Never"}
582
510
  </span>
583
511
  </div>
584
512
  {idx < group.connectedDevices.length - 1 && (
@@ -1,6 +1,6 @@
1
- import type { IConsoleLog } from "@peers-app/peers-sdk";
1
+ import type { IConsoleLog, INetworkRemoteLogsResult } from "@peers-app/peers-sdk";
2
2
  import { useCallback, useState } from "react";
3
- import { getNetworkViewerApi, type RemoteLogsResult } from "./network-viewer-ipc";
3
+ import { getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
4
4
 
5
5
  interface RemoteLogsPanelProps {
6
6
  deviceId: string;
@@ -34,7 +34,7 @@ function RemoteLogRow({ log }: { log: IConsoleLog }) {
34
34
 
35
35
  /** Read-only System Logs contract panel for a directly connected, provider-authorized device. */
36
36
  export function RemoteLogsPanel({ deviceId }: RemoteLogsPanelProps) {
37
- const [result, setResult] = useState<RemoteLogsResult | null>(null);
37
+ const [result, setResult] = useState<INetworkRemoteLogsResult | null>(null);
38
38
  const [loading, setLoading] = useState(false);
39
39
  const [error, setError] = useState<string>();
40
40
  const [processName, setProcessName] = useState("");
@@ -43,12 +43,6 @@ export function RemoteLogsPanel({ deviceId }: RemoteLogsPanelProps) {
43
43
  const [minutes, setMinutes] = useState("60");
44
44
 
45
45
  const loadLogs = useCallback(async () => {
46
- const api = getNetworkViewerApi();
47
- if (!api) {
48
- setError("Network Viewer API is unavailable");
49
- return;
50
- }
51
-
52
46
  setLoading(true);
53
47
  setError(undefined);
54
48
  try {
@@ -57,13 +51,17 @@ export function RemoteLogsPanel({ deviceId }: RemoteLogsPanelProps) {
57
51
  Number.isFinite(minutesNumber) && minutesNumber > 0
58
52
  ? Date.now() - minutesNumber * 60_000
59
53
  : undefined;
60
- const response = await api.getRemoteLogs(deviceId, {
61
- sinceTimestamp,
62
- level: level ? (level as IConsoleLog["level"]) : undefined,
63
- process: processName || undefined,
64
- search: search || undefined,
65
- pageSize: 100,
66
- timeoutMs: 10_000,
54
+ const diagnostics = await getNetworkDiagnosticsConsumer();
55
+ const response = await diagnostics.tools.queryRemoteLogs({
56
+ deviceId,
57
+ query: {
58
+ sinceTimestamp,
59
+ level: level ? (level as IConsoleLog["level"]) : undefined,
60
+ process: processName || undefined,
61
+ search: search || undefined,
62
+ pageSize: 100,
63
+ timeoutMs: 10_000,
64
+ },
67
65
  });
68
66
  setResult(response);
69
67
  } catch (cause) {