@peers-app/peers-ui 0.21.2 → 0.21.4

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 +41 -0
  11. package/dist/tabs-layout/network-status-indicator.js +261 -0
  12. package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
  13. package/dist/tabs-layout/network-status-indicator.test.js +333 -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 +391 -0
  28. package/src/tabs-layout/network-status-indicator.tsx +475 -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
@@ -6,7 +6,6 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
6
  const react_1 = require("react");
7
7
  const loading_indicator_1 = require("../../components/loading-indicator");
8
8
  const trust_level_badge_1 = require("../../components/trust-level-badge");
9
- const globals_1 = require("../../globals");
10
9
  const ui_loader_1 = require("../../ui-router/ui-loader");
11
10
  const connection_troubleshooter_1 = require("./connection-troubleshooter");
12
11
  const device_details_modal_1 = require("./device-details-modal");
@@ -47,18 +46,12 @@ function NetworkViewerList() {
47
46
  const [showTroubleshooter, setShowTroubleshooter] = (0, react_1.useState)(false);
48
47
  const loadData = (0, react_1.useCallback)(async () => {
49
48
  try {
50
- if (!(0, globals_1.isDesktop)())
51
- return;
52
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
53
- if (!api) {
54
- console.warn("Network Viewer API not available");
55
- return;
56
- }
57
- const [overviewData, connectionsData, groupSyncData, disconnectedData] = await Promise.all([
58
- api.getOverview(),
59
- api.getConnections(),
60
- api.getGroupSyncStatus(),
61
- api.getDisconnectedDevices(),
49
+ const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
50
+ const [{ overview: overviewData }, { connections: connectionsData }, { groups: groupSyncData }, { devices: disconnectedData },] = await Promise.all([
51
+ diagnostics.tools.getOverview(),
52
+ diagnostics.tools.getConnections(),
53
+ diagnostics.tools.getGroupSyncStatus(),
54
+ diagnostics.tools.getDisconnectedDevices(),
62
55
  ]);
63
56
  setOverview(overviewData);
64
57
  setConnections(connectionsData);
@@ -75,11 +68,14 @@ function NetworkViewerList() {
75
68
  const handleRefresh = async () => {
76
69
  setRefreshing(true);
77
70
  try {
78
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
79
- if (api) {
80
- await api.refreshNetworkInfo();
71
+ const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
72
+ const result = await control.tools.refreshNetworkInfo();
73
+ if (result.success) {
81
74
  await loadData();
82
75
  }
76
+ else {
77
+ alert(result.message);
78
+ }
83
79
  }
84
80
  catch (error) {
85
81
  console.error("Error refreshing network info:", error);
@@ -93,11 +89,14 @@ function NetworkViewerList() {
93
89
  return;
94
90
  }
95
91
  try {
96
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
97
- if (api) {
98
- await api.disconnectDevice(deviceId);
92
+ const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
93
+ const result = await control.tools.disconnectDevice({ deviceId });
94
+ if (result.success) {
99
95
  await loadData();
100
96
  }
97
+ else {
98
+ alert(result.message);
99
+ }
101
100
  }
102
101
  catch (error) {
103
102
  console.error("Error disconnecting device:", error);
@@ -108,17 +107,14 @@ function NetworkViewerList() {
108
107
  };
109
108
  const handleConnectToDevice = async (deviceId) => {
110
109
  try {
111
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
112
- if (api) {
113
- setConnecting((prev) => new Set(prev).add(deviceId));
114
- const result = await api.connectToDevice(deviceId);
115
- if (result.success) {
116
- // Refresh data to see if connection was established
117
- await loadData();
118
- }
119
- else {
120
- alert(`Failed to connect: ${result.message}`);
121
- }
110
+ const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
111
+ setConnecting((prev) => new Set(prev).add(deviceId));
112
+ const result = await control.tools.connectToDevice({ deviceId });
113
+ if (result.success) {
114
+ await loadData();
115
+ }
116
+ else {
117
+ alert(`Failed to connect: ${result.message}`);
122
118
  }
123
119
  }
124
120
  catch (error) {
@@ -135,16 +131,13 @@ function NetworkViewerList() {
135
131
  };
136
132
  const handleDeleteDevice = async (deviceId) => {
137
133
  try {
138
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
139
- if (api) {
140
- const result = await api.deleteDevice(deviceId);
141
- if (result.success) {
142
- // Refresh data to remove the device from the list
143
- await loadData();
144
- }
145
- else {
146
- alert(`Failed to delete device: ${result.message}`);
147
- }
134
+ const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
135
+ const result = await control.tools.deleteDevice({ deviceId });
136
+ if (result.success) {
137
+ await loadData();
138
+ }
139
+ else {
140
+ alert(`Failed to delete device: ${result.message}`);
148
141
  }
149
142
  }
150
143
  catch (error) {
@@ -161,41 +154,32 @@ function NetworkViewerList() {
161
154
  if (loading) {
162
155
  return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
163
156
  }
164
- if (!(0, globals_1.isDesktop)()) {
165
- return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid p-4", children: (0, jsx_runtime_1.jsx)("div", { className: "alert alert-info", children: "Network Viewer is only available in the desktop application." }) }));
166
- }
167
157
  if (!overview) {
168
158
  return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid p-4", children: (0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Unable to load network information." }) }));
169
159
  }
170
160
  if (showTroubleshooter) {
171
161
  return (0, jsx_runtime_1.jsx)(connection_troubleshooter_1.ConnectionTroubleshooter, { onBack: () => setShowTroubleshooter(false) });
172
162
  }
173
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Network Viewer" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-warning", onClick: () => setShowTroubleshooter(true), title: "Run connection diagnostics", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-stethoscope me-1" }), "Troubleshoot"] }), (0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-primary", onClick: handleRefresh, disabled: refreshing, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi bi-arrow-clockwise ${refreshing ? "spin" : ""}` }), refreshing ? " Refreshing..." : " Refresh"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "card mb-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Local Device" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localDeviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "User ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localUserId })] })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "row mb-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Connections", fetchUsage: async () => {
174
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
175
- if (!api)
176
- return 0;
177
- const data = (await api.getOverview());
178
- return data.totalConnections;
163
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Network Viewer" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, network_viewer_ipc_1.getNetworkViewerPlatformApi)()?.runDiagnostics && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-warning", onClick: () => setShowTroubleshooter(true), title: "Run connection diagnostics", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-stethoscope me-1" }), "Troubleshoot"] })), (0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-primary", onClick: handleRefresh, disabled: refreshing, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi bi-arrow-clockwise ${refreshing ? "spin" : ""}` }), refreshing ? " Refreshing..." : " Refresh"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "card mb-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Local Device" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localDeviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "User ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localUserId })] })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "row mb-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Connections", fetchUsage: async () => {
164
+ const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
165
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
166
+ return currentOverview.totalConnections;
179
167
  }, durationMinutes: 5, updateInterval: 5000, color: "rgba(111, 66, 193, 1)", colorZones: [
180
168
  { maxValue: 1, color: "rgba(220, 53, 69, 1)" },
181
169
  { maxValue: 2, color: "rgba(255, 193, 7, 1)" },
182
170
  { maxValue: overview?.maxConnections || 30, color: "rgba(111, 66, 193, 1)" },
183
171
  ], minValue: 0, maxValue: overview?.maxConnections || 30, formatOverlayText: (value) => `Connections: ${Math.round(value)}` }) }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "CPU", fetchUsage: async () => {
184
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
185
- if (!api)
186
- return 0;
187
- const data = (await api.getOverview());
188
- return data.cpuPercent;
172
+ const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
173
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
174
+ return currentOverview.cpuPercent;
189
175
  }, durationMinutes: 5, updateInterval: 5000, color: "rgba(13, 110, 253, 1)", colorZones: [
190
176
  { maxValue: 60, color: "rgba(13, 110, 253, 1)" },
191
177
  { maxValue: 80, color: "rgba(255, 193, 7, 1)" },
192
178
  { maxValue: Infinity, color: "rgba(220, 53, 69, 1)" },
193
179
  ] }) }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Memory", fetchUsage: async () => {
194
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
195
- if (!api)
196
- return 0;
197
- const data = (await api.getOverview());
198
- return data.memPercent;
180
+ const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
181
+ const { overview: currentOverview } = await diagnostics.tools.getOverview();
182
+ return currentOverview.memPercent;
199
183
  }, durationMinutes: 5, updateInterval: 5000, color: "rgba(25, 135, 84, 1)", colorZones: [
200
184
  { maxValue: 60, color: "rgba(25, 135, 84, 1)" },
201
185
  { maxValue: 80, color: "rgba(255, 193, 7, 1)" },
@@ -218,7 +202,9 @@ function NetworkViewerList() {
218
202
  ? "success"
219
203
  : device.errorRate < 0.2
220
204
  ? "warning"
221
- : "danger"}`, children: [(device.errorRate * 100).toFixed(1), "% errors"] }), (0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info", children: ["Last: ", new Date(device.timestampLastApplied).toLocaleString()] })] }), idx < group.connectedDevices.length - 1 && ((0, jsx_runtime_1.jsx)("hr", { className: "my-2" }))] }, device.deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "No connected devices" })) }), (0, jsx_runtime_1.jsx)("td", { children: group.preferredDeviceIds.length > 0 ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("span", { className: "badge bg-primary mb-2", children: [group.preferredDeviceIds.length, " preferred"] }), (0, jsx_runtime_1.jsx)("div", { children: group.preferredDeviceIds.map((deviceId) => ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("small", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted", children: deviceId }) }) }, deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "None" })) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("small", { children: group.lastSyncTimestamp > 0
205
+ : "danger"}`, children: [(device.errorRate * 100).toFixed(1), "% errors"] }), (0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info", children: ["Last:", " ", device.timestampLastApplied
206
+ ? new Date(device.timestampLastApplied).toLocaleString()
207
+ : "Never"] })] }), idx < group.connectedDevices.length - 1 && ((0, jsx_runtime_1.jsx)("hr", { className: "my-2" }))] }, device.deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "No connected devices" })) }), (0, jsx_runtime_1.jsx)("td", { children: group.preferredDeviceIds.length > 0 ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("span", { className: "badge bg-primary mb-2", children: [group.preferredDeviceIds.length, " preferred"] }), (0, jsx_runtime_1.jsx)("div", { children: group.preferredDeviceIds.map((deviceId) => ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("small", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted", children: deviceId }) }) }, deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "None" })) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("small", { children: group.lastSyncTimestamp > 0
222
208
  ? new Date(group.lastSyncTimestamp).toLocaleString()
223
209
  : "Never" }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-primary", onClick: () => setSelectedGroupId(group.groupId), title: "View Group Details", children: group.totalMembers }) })] }, group.groupId))) })] }) }))] }) }), (0, jsx_runtime_1.jsx)("div", { className: "card", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Disconnected Devices" }), disconnectedDevices.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "No known disconnected devices" })) : ((0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "User" }), (0, jsx_runtime_1.jsx)("th", { children: "Trust Level" }), (0, jsx_runtime_1.jsx)("th", { children: "Device ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Device Name" }), (0, jsx_runtime_1.jsx)("th", { children: "First Seen" }), (0, jsx_runtime_1.jsx)("th", { children: "Last Seen" }), (0, jsx_runtime_1.jsx)("th", { children: "Actions" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: disconnectedDevices
224
210
  .sort((a, b) => new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime())
@@ -17,11 +17,6 @@ function RemoteLogsPanel({ deviceId }) {
17
17
  const [search, setSearch] = (0, react_1.useState)("");
18
18
  const [minutes, setMinutes] = (0, react_1.useState)("60");
19
19
  const loadLogs = (0, react_1.useCallback)(async () => {
20
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
21
- if (!api) {
22
- setError("Network Viewer API is unavailable");
23
- return;
24
- }
25
20
  setLoading(true);
26
21
  setError(undefined);
27
22
  try {
@@ -29,13 +24,17 @@ function RemoteLogsPanel({ deviceId }) {
29
24
  const sinceTimestamp = Number.isFinite(minutesNumber) && minutesNumber > 0
30
25
  ? Date.now() - minutesNumber * 60_000
31
26
  : undefined;
32
- const response = await api.getRemoteLogs(deviceId, {
33
- sinceTimestamp,
34
- level: level ? level : undefined,
35
- process: processName || undefined,
36
- search: search || undefined,
37
- pageSize: 100,
38
- timeoutMs: 10_000,
27
+ const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
28
+ const response = await diagnostics.tools.queryRemoteLogs({
29
+ deviceId,
30
+ query: {
31
+ sinceTimestamp,
32
+ level: level ? level : undefined,
33
+ process: processName || undefined,
34
+ search: search || undefined,
35
+ pageSize: 100,
36
+ timeoutMs: 10_000,
37
+ },
39
38
  });
40
39
  setResult(response);
41
40
  }
@@ -0,0 +1,41 @@
1
+ import { type INetworkStatusViewState } from "./use-network-status";
2
+ /** How long continuous raw syncing stays a green pulse before the indeterminate circle. */
3
+ export declare const SYNCING_GREEN_MS = 2000;
4
+ /** How long status must stay fully synced before leaving the syncing visual. */
5
+ export declare const SYNCED_STABLE_MS = 1000;
6
+ type NetworkStatusMark = "solid" | "pulse" | "indeterminate";
7
+ interface INetworkStatusPresentation {
8
+ label: string;
9
+ shortLabel: string;
10
+ colorClass: string;
11
+ /** Bootstrap Icons class when not using a status mark. */
12
+ iconClass?: string;
13
+ spinning?: boolean;
14
+ /** Size the Bootstrap icon to match the status marks. */
15
+ compactIcon?: boolean;
16
+ /** Solid / pulsing / indeterminate mark (syncing / error). */
17
+ mark?: NetworkStatusMark;
18
+ }
19
+ /** Convert host availability and network state into accessible indicator text and styling. */
20
+ export declare function getNetworkStatusPresentation(viewState: INetworkStatusViewState, opts?: {
21
+ syncingLongerThanGreenMs?: boolean;
22
+ /** Keep the calm green syncing pulse while waiting for stable synced. */
23
+ holdSyncingVisual?: boolean;
24
+ }): INetworkStatusPresentation;
25
+ /** Compact relative time for the status popover (always short enough for one line). */
26
+ export declare function formatRelativeSyncTime(timestampMs: number, nowMs?: number): string;
27
+ /** Prefer a friendly name; otherwise a compact middle-ellipsis of the id. */
28
+ export declare function connectionIdentityLabel(name: string | undefined, id: string): {
29
+ text: string;
30
+ isId: boolean;
31
+ title: string;
32
+ };
33
+ /** Accessible reactive network status button and details popover. */
34
+ export declare function NetworkStatusIndicator({ compact }: {
35
+ compact?: boolean;
36
+ }): import("react/jsx-runtime").JSX.Element;
37
+ /** Fixed tabs-strip area for built-in shell indicators. */
38
+ export declare function TabsStripIndicators({ compact }: {
39
+ compact?: boolean;
40
+ }): import("react/jsx-runtime").JSX.Element;
41
+ export {};
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SYNCED_STABLE_MS = exports.SYNCING_GREEN_MS = void 0;
7
+ exports.getNetworkStatusPresentation = getNetworkStatusPresentation;
8
+ exports.formatRelativeSyncTime = formatRelativeSyncTime;
9
+ exports.connectionIdentityLabel = connectionIdentityLabel;
10
+ exports.NetworkStatusIndicator = NetworkStatusIndicator;
11
+ exports.TabsStripIndicators = TabsStripIndicators;
12
+ const jsx_runtime_1 = require("react/jsx-runtime");
13
+ const moment_1 = __importDefault(require("moment"));
14
+ const react_1 = require("react");
15
+ const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
16
+ const tabs_state_1 = require("./tabs-state");
17
+ const use_network_status_1 = require("./use-network-status");
18
+ /** How long continuous raw syncing stays a green pulse before the indeterminate circle. */
19
+ exports.SYNCING_GREEN_MS = 2_000;
20
+ /** How long status must stay fully synced before leaving the syncing visual. */
21
+ exports.SYNCED_STABLE_MS = 1_000;
22
+ /** Slightly slower than Bootstrap's default spinner-grow (~0.75s). */
23
+ const PULSE_ANIMATION_DURATION = "1.0s";
24
+ const DOT_SIZE = "0.7rem";
25
+ /** Convert host availability and network state into accessible indicator text and styling. */
26
+ function getNetworkStatusPresentation(viewState, opts) {
27
+ if (viewState.availability === "loading") {
28
+ return {
29
+ label: "Network status loading",
30
+ shortLabel: "Loading",
31
+ iconClass: "bi-arrow-repeat",
32
+ colorClass: "text-secondary",
33
+ spinning: true,
34
+ };
35
+ }
36
+ if (viewState.availability === "unavailable" || !viewState.status) {
37
+ return {
38
+ label: "Network status unavailable",
39
+ shortLabel: "Unavailable",
40
+ iconClass: "bi-question-circle",
41
+ colorClass: "text-secondary",
42
+ };
43
+ }
44
+ const { connectionCount, errorCount, state, syncingContextCount } = viewState.status;
45
+ if (state === "error") {
46
+ return {
47
+ label: `Network error; ${errorCount} sync ${errorCount === 1 ? "error" : "errors"}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
48
+ shortLabel: "Error",
49
+ colorClass: "text-danger",
50
+ mark: "solid",
51
+ };
52
+ }
53
+ if (state === "syncing" || opts?.holdSyncingVisual) {
54
+ const longSync = state === "syncing" && opts?.syncingLongerThanGreenMs === true;
55
+ const contextsLabel = state === "syncing"
56
+ ? `${syncingContextCount} ${syncingContextCount === 1 ? "context" : "contexts"}`
57
+ : "recent activity";
58
+ return {
59
+ label: `Network syncing ${contextsLabel}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
60
+ shortLabel: "Syncing",
61
+ colorClass: longSync ? "text-primary" : "text-success",
62
+ mark: longSync ? "indeterminate" : "pulse",
63
+ };
64
+ }
65
+ if (state === "offline") {
66
+ return {
67
+ label: "Network offline; no active connections",
68
+ shortLabel: "Offline",
69
+ iconClass: "bi-cloud-slash-fill",
70
+ colorClass: "text-secondary",
71
+ };
72
+ }
73
+ return {
74
+ label: `Network synced; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
75
+ shortLabel: "Synced",
76
+ iconClass: "bi-check-circle-fill",
77
+ colorClass: "text-success",
78
+ compactIcon: true,
79
+ };
80
+ }
81
+ function StatusMark({ mode, colorClass }) {
82
+ if (mode === "pulse") {
83
+ return ((0, jsx_runtime_1.jsx)("span", { className: `spinner-grow spinner-grow-sm ${colorClass}`, style: {
84
+ width: DOT_SIZE,
85
+ height: DOT_SIZE,
86
+ animationDuration: PULSE_ANIMATION_DURATION,
87
+ }, "aria-hidden": "true" }));
88
+ }
89
+ if (mode === "indeterminate") {
90
+ return ((0, jsx_runtime_1.jsx)("span", { className: `spinner-border spinner-border-sm ${colorClass}`, style: { width: DOT_SIZE, height: DOT_SIZE, borderWidth: "0.12em" }, "aria-hidden": "true" }));
91
+ }
92
+ const bgClass = colorClass === "text-danger"
93
+ ? "bg-danger"
94
+ : colorClass === "text-primary"
95
+ ? "bg-primary"
96
+ : "bg-success";
97
+ return ((0, jsx_runtime_1.jsx)("span", { className: `rounded-circle ${bgClass}`, style: { width: DOT_SIZE, height: DOT_SIZE, display: "inline-block" }, "aria-hidden": "true" }));
98
+ }
99
+ /** Compact relative time for the status popover (always short enough for one line). */
100
+ function formatRelativeSyncTime(timestampMs, nowMs = Date.now()) {
101
+ const then = (0, moment_1.default)(timestampMs);
102
+ const now = (0, moment_1.default)(nowMs);
103
+ if (now.diff(then, "seconds") < 45) {
104
+ return "now";
105
+ }
106
+ if (now.clone().subtract(1, "day").isSame(then, "day")) {
107
+ return "yesterday";
108
+ }
109
+ return then.from(now);
110
+ }
111
+ function middleEllipsis(value, head = 4, tail = 4) {
112
+ if (value.length <= head + tail + 1) {
113
+ return value;
114
+ }
115
+ return `${value.slice(0, head)}…${value.slice(-tail)}`;
116
+ }
117
+ /** Prefer a friendly name; otherwise a compact middle-ellipsis of the id. */
118
+ function connectionIdentityLabel(name, id) {
119
+ const trimmed = name?.trim();
120
+ if (trimmed) {
121
+ return { text: trimmed, isId: false, title: trimmed };
122
+ }
123
+ return { text: middleEllipsis(id), isId: true, title: id };
124
+ }
125
+ function IdentityPart({ label }) {
126
+ return ((0, jsx_runtime_1.jsx)("span", { className: `text-truncate ${label.isId ? "text-muted" : ""}`, style: {
127
+ minWidth: 0,
128
+ ...(label.isId ? { fontSize: "0.7rem" } : undefined),
129
+ }, title: label.title, children: label.text }));
130
+ }
131
+ function ConnectedDeviceRow({ connection }) {
132
+ const user = connectionIdentityLabel(connection.userName, connection.userId);
133
+ const device = connectionIdentityLabel(connection.deviceName, connection.deviceId);
134
+ const groupCount = connection.sharedGroups.length;
135
+ return ((0, jsx_runtime_1.jsxs)("li", { className: "d-flex align-items-baseline gap-1 py-1", style: { listStyle: "none", minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)(IdentityPart, { label: user }), (0, jsx_runtime_1.jsx)("span", { className: "text-muted flex-shrink-0", "aria-hidden": "true", children: "\u00B7" }), (0, jsx_runtime_1.jsx)(IdentityPart, { label: device }), groupCount > 0 && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted flex-shrink-0 ms-auto", style: { fontSize: "0.7rem" }, title: `${groupCount} ${groupCount === 1 ? "group" : "groups"}`, children: [groupCount, "g"] }))] }));
136
+ }
137
+ function ConnectedDevicesList({ loadState }) {
138
+ if (loadState.status === "idle" || loadState.status === "loading") {
139
+ return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "Loading\u2026" });
140
+ }
141
+ if (loadState.status === "error") {
142
+ return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "Couldn't load devices" });
143
+ }
144
+ if (loadState.connections.length === 0) {
145
+ return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "No connected devices" });
146
+ }
147
+ return ((0, jsx_runtime_1.jsx)("ul", { className: "small mb-0 ps-0", style: { maxHeight: "180px", overflowY: "auto" }, children: loadState.connections.map((connection) => ((0, jsx_runtime_1.jsx)(ConnectedDeviceRow, { connection: connection }, connection.deviceId))) }));
148
+ }
149
+ function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onOpenViewer, }) {
150
+ return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
151
+ }
152
+ /** Accessible reactive network status button and details popover. */
153
+ function NetworkStatusIndicator({ compact = false }) {
154
+ const viewState = (0, use_network_status_1.useNetworkStatus)();
155
+ const [displaySyncing, setDisplaySyncing] = (0, react_1.useState)(false);
156
+ const [syncingLongerThanGreenMs, setSyncingLongerThanGreenMs] = (0, react_1.useState)(false);
157
+ const [open, setOpen] = (0, react_1.useState)(false);
158
+ const [connectionsLoadState, setConnectionsLoadState] = (0, react_1.useState)({
159
+ status: "idle",
160
+ });
161
+ const containerRef = (0, react_1.useRef)(null);
162
+ const detailsId = (0, react_1.useId)();
163
+ const rawState = viewState.availability === "ready" ? viewState.status?.state : undefined;
164
+ const rawIsSyncing = rawState === "syncing";
165
+ const rawIsSynced = rawState === "synced";
166
+ const connectionCount = viewState.status?.connectionCount;
167
+ const diagnosticsReady = viewState.availability === "ready";
168
+ (0, react_1.useEffect)(() => {
169
+ if (rawIsSyncing) {
170
+ setDisplaySyncing(true);
171
+ return;
172
+ }
173
+ if (rawIsSynced) {
174
+ if (!displaySyncing) {
175
+ return;
176
+ }
177
+ const timer = window.setTimeout(() => {
178
+ setDisplaySyncing(false);
179
+ }, exports.SYNCED_STABLE_MS);
180
+ return () => {
181
+ window.clearTimeout(timer);
182
+ };
183
+ }
184
+ setDisplaySyncing(false);
185
+ }, [rawIsSyncing, rawIsSynced, displaySyncing]);
186
+ (0, react_1.useEffect)(() => {
187
+ if (!rawIsSyncing) {
188
+ setSyncingLongerThanGreenMs(false);
189
+ return;
190
+ }
191
+ const timer = window.setTimeout(() => {
192
+ setSyncingLongerThanGreenMs(true);
193
+ }, exports.SYNCING_GREEN_MS);
194
+ return () => {
195
+ window.clearTimeout(timer);
196
+ };
197
+ }, [rawIsSyncing]);
198
+ (0, react_1.useEffect)(() => {
199
+ if (!open || !diagnosticsReady) {
200
+ setConnectionsLoadState({ status: "idle" });
201
+ return;
202
+ }
203
+ let active = true;
204
+ setConnectionsLoadState({ status: "loading" });
205
+ void (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)()
206
+ .then((diagnostics) => diagnostics.tools.getConnections())
207
+ .then((result) => {
208
+ if (active) {
209
+ setConnectionsLoadState({ status: "ready", connections: result.connections });
210
+ }
211
+ })
212
+ .catch(() => {
213
+ if (active) {
214
+ setConnectionsLoadState({ status: "error" });
215
+ }
216
+ });
217
+ return () => {
218
+ active = false;
219
+ };
220
+ }, [open, diagnosticsReady, connectionCount]);
221
+ const holdSyncingVisual = displaySyncing && !rawIsSyncing;
222
+ const presentation = getNetworkStatusPresentation(viewState, {
223
+ syncingLongerThanGreenMs,
224
+ holdSyncingVisual,
225
+ });
226
+ (0, react_1.useEffect)(() => {
227
+ if (!open) {
228
+ return;
229
+ }
230
+ const closeOutside = (event) => {
231
+ if (!containerRef.current?.contains(event.target)) {
232
+ setOpen(false);
233
+ }
234
+ };
235
+ const closeOnEscape = (event) => {
236
+ if (event.key === "Escape") {
237
+ setOpen(false);
238
+ }
239
+ };
240
+ document.addEventListener("mousedown", closeOutside);
241
+ document.addEventListener("keydown", closeOnEscape);
242
+ return () => {
243
+ document.removeEventListener("mousedown", closeOutside);
244
+ document.removeEventListener("keydown", closeOnEscape);
245
+ };
246
+ }, [open]);
247
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: "position-relative d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `btn btn-sm d-flex align-items-center gap-1 px-2 ${presentation.colorClass}`, style: {
248
+ border: "none",
249
+ background: "transparent",
250
+ minWidth: compact ? "30px" : undefined,
251
+ }, "aria-label": presentation.label, "aria-expanded": open, "aria-controls": detailsId, title: presentation.label, onClick: () => setOpen((current) => !current), children: [presentation.mark ? ((0, jsx_runtime_1.jsx)(StatusMark, { mode: presentation.mark, colorClass: presentation.colorClass })) : ((0, jsx_runtime_1.jsx)("i", { className: `bi ${presentation.iconClass} ${presentation.spinning ? "spin" : ""}`, style: presentation.compactIcon
252
+ ? { fontSize: DOT_SIZE, lineHeight: 1, display: "inline-block" }
253
+ : undefined, "aria-hidden": "true" })), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onOpenViewer: () => {
254
+ setOpen(false);
255
+ (0, tabs_state_1.goToTabPath)("network-viewer");
256
+ } }))] }));
257
+ }
258
+ /** Fixed tabs-strip area for built-in shell indicators. */
259
+ function TabsStripIndicators({ compact = false }) {
260
+ return ((0, jsx_runtime_1.jsx)("div", { className: "d-flex align-items-center flex-shrink-0", role: "group", "aria-label": "Application status", children: (0, jsx_runtime_1.jsx)(NetworkStatusIndicator, { compact: compact }) }));
261
+ }