@peers-app/peers-ui 0.19.13 → 0.20.0

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.
@@ -5,5 +5,6 @@ interface TrustLevelDropdownProps {
5
5
  onChange?: (newLevel: TrustLevel) => void;
6
6
  type?: "user" | "device";
7
7
  }
8
+ /** Select a context-scoped user or device trust level, including the full-access Self grant. */
8
9
  export declare const TrustLevelDropdown: ({ userId, dataContext, onChange, type, }: TrustLevelDropdownProps) => import("react/jsx-runtime").JSX.Element;
9
10
  export {};
@@ -5,11 +5,17 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const peers_sdk_1 = require("@peers-app/peers-sdk");
6
6
  const react_1 = require("react");
7
7
  const loading_indicator_1 = require("./loading-indicator");
8
+ /** Select a context-scoped user or device trust level, including the full-access Self grant. */
8
9
  const TrustLevelDropdown = ({ userId, dataContext, onChange, type = "user", }) => {
9
10
  const [trustLevel, setTrustLevelState] = (0, react_1.useState)(peers_sdk_1.TrustLevel.Unknown);
10
11
  const [loading, setLoading] = (0, react_1.useState)(true);
11
12
  const allTrustLevelConfigs = [
12
- { value: peers_sdk_1.TrustLevel.Self, color: "success", icon: "bi-shield-fill-check", text: "Self" },
13
+ {
14
+ value: peers_sdk_1.TrustLevel.Self,
15
+ color: "success",
16
+ icon: "bi-shield-fill-check",
17
+ text: "Self (full remote contract access)",
18
+ },
13
19
  { value: peers_sdk_1.TrustLevel.Trusted, color: "success", icon: "bi-shield-check", text: "Trusted" },
14
20
  { value: peers_sdk_1.TrustLevel.Known, color: "primary", icon: "bi-shield-check", text: "Known" },
15
21
  {
@@ -1,5 +1,6 @@
1
1
  interface IProps {
2
2
  userId: string;
3
3
  }
4
+ /** Edit a contact's signed profile fields and personal-context trust assignment. */
4
5
  export declare const ContactDetails: (props: IProps) => import("react/jsx-runtime").JSX.Element;
5
6
  export {};
@@ -11,6 +11,7 @@ const trust_level_dropdown_1 = require("../../components/trust-level-dropdown");
11
11
  const hooks_1 = require("../../hooks");
12
12
  const tabs_state_1 = require("../../tabs-layout/tabs-state");
13
13
  const ui_loader_1 = require("../../ui-router/ui-loader");
14
+ /** Edit a contact's signed profile fields and personal-context trust assignment. */
14
15
  const ContactDetails = (props) => {
15
16
  const contact = (0, hooks_1.usePromise)(async () => {
16
17
  const userContext = await (0, peers_sdk_1.getUserContext)();
@@ -34,7 +35,7 @@ const ContactDetails = (props) => {
34
35
  exports.ContactDetails = ContactDetails;
35
36
  const ContactInfo = (props) => {
36
37
  const { contact } = props;
37
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Name" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.name, className: "form-control mb-3 ps-2", placeholder: "Contact name", title: "Contact name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "User ID" }), (0, jsx_runtime_1.jsx)("code", { className: "small", children: contact.userId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Trust Level" }), (0, jsx_runtime_1.jsx)(trust_level_dropdown_1.TrustLevelDropdown, { userId: contact.userId })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public key", title: "Public key" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Box Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicBoxKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public box key", title: "Public box key" })] })] }));
38
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Name" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.name, className: "form-control mb-3 ps-2", placeholder: "Contact name", title: "Contact name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "User ID" }), (0, jsx_runtime_1.jsx)("code", { className: "small", children: contact.userId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "border-start ps-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mb-1", children: "Trust Level" }), (0, jsx_runtime_1.jsx)(trust_level_dropdown_1.TrustLevelDropdown, { userId: contact.userId }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block mt-1", children: "Self grants this user full remote contract access." })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public key", title: "Public key" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { children: "Public Box Key:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: contact.qs.publicBoxKey, className: "form-control mb-3 p-0 ps-2", placeholder: "Public box key", title: "Public box key" })] })] }));
38
39
  };
39
40
  (0, ui_loader_1.registerInternalPeersUI)({
40
41
  peersUIId: "00contact00details00component",
@@ -3,5 +3,6 @@ interface IDeviceDetailsModalProps {
3
3
  onClose: () => void;
4
4
  onDisconnect?: () => void;
5
5
  }
6
+ /** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
6
7
  export declare function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceDetailsModalProps): import("react/jsx-runtime").JSX.Element;
7
8
  export {};
@@ -7,6 +7,7 @@ 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
9
  const network_viewer_ipc_1 = require("./network-viewer-ipc");
10
+ const remote_logs_panel_1 = require("./remote-logs-panel");
10
11
  /** Format bytes to human-readable string (KB, MB, GB) */
11
12
  function formatBytes(bytes) {
12
13
  if (bytes < 1024)
@@ -28,6 +29,7 @@ function formatRate(mbps) {
28
29
  }
29
30
  return `${mbps.toFixed(2)} MB/s`;
30
31
  }
32
+ /** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
31
33
  function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
32
34
  const [device, setDevice] = (0, react_1.useState)(null);
33
35
  const [loading, setLoading] = (0, react_1.useState)(true);
@@ -86,7 +88,7 @@ function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
86
88
  overflowY: "auto",
87
89
  paddingTop: "2rem",
88
90
  paddingBottom: "2rem",
89
- }, onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { className: "card", style: { maxWidth: "900px", width: "90%", maxHeight: "90vh" }, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0", children: loading ? "Loading..." : device?.userName || device?.userId || "Device Details" }), device?.userName && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: (0, jsx_runtime_1.jsx)("code", { children: device.userId }) }))] }), (0, jsx_runtime_1.jsxs)("div", { children: [device && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-danger me-2", onClick: handleDisconnect, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-x-circle" }), " Disconnect"] })), (0, jsx_runtime_1.jsx)("button", { className: "btn-close", onClick: onClose, "aria-label": "Close" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body", style: { overflowY: "auto", maxHeight: "calc(90vh - 120px)" }, children: [loading && (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}), !loading && (notFound || !device) && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Device not found or connection has been closed." })), !loading && device && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Connection Information" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [device.deviceName && ((0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device Name:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: device.deviceName })] })), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: device.deviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Connection State:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: "badge bg-success", children: device.connectionState })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Trust Level:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: device.trustLevel || peers_sdk_1.TrustLevel.Unknown })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Path Type:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: `badge bg-${device.pathType === "direct" ? "success" : "info"}`, children: device.pathType })] }), device.throughDeviceIds && device.throughDeviceIds.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "col-md-12 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Path:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: device.throughDeviceIds.join(" → ") })] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Throughput" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Send Rate:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("span", { className: "text-success", children: ["\u2191 ", formatRate(device.sendRateMBps)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Receive Rate:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("span", { className: "text-primary", children: ["\u2193 ", formatRate(device.receiveRateMBps)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Sent:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: formatBytes(device.bytesSent || 0) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Received:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: formatBytes(device.bytesReceived || 0) })] })] })] }), device.sharedGroups.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: ["Shared Groups (", device.sharedGroups.length, ")"] }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Group ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Role" }), (0, jsx_runtime_1.jsx)("th", { children: "Sync Status" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: device.sharedGroups.map((groupId) => {
91
+ }, onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { className: "card", style: { maxWidth: "900px", width: "90%", maxHeight: "90vh" }, onClick: (e) => e.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0", children: loading ? "Loading..." : device?.userName || device?.userId || "Device Details" }), device?.userName && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: (0, jsx_runtime_1.jsx)("code", { children: device.userId }) }))] }), (0, jsx_runtime_1.jsxs)("div", { children: [device && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-danger me-2", onClick: handleDisconnect, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-x-circle" }), " Disconnect"] })), (0, jsx_runtime_1.jsx)("button", { className: "btn-close", onClick: onClose, "aria-label": "Close" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body", style: { overflowY: "auto", maxHeight: "calc(90vh - 120px)" }, children: [loading && (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}), !loading && (notFound || !device) && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Device not found or connection has been closed." })), !loading && device && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Connection Information" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [device.deviceName && ((0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device Name:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: device.deviceName })] })), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: device.deviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Connection State:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: "badge bg-success", children: device.connectionState })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Trust Level:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: device.trustLevel || peers_sdk_1.TrustLevel.Unknown })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Path Type:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { className: `badge bg-${device.pathType === "direct" ? "success" : "info"}`, children: device.pathType })] }), device.throughDeviceIds && device.throughDeviceIds.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "col-md-12 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Path:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: device.throughDeviceIds.join(" → ") })] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("h6", { className: "border-bottom pb-2 mb-3", children: "Throughput" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Send Rate:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("span", { className: "text-success", children: ["\u2191 ", formatRate(device.sendRateMBps)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Receive Rate:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("span", { className: "text-primary", children: ["\u2193 ", formatRate(device.receiveRateMBps)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Sent:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: formatBytes(device.bytesSent || 0) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Total Received:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("span", { children: formatBytes(device.bytesReceived || 0) })] })] })] }), device.canReadRemoteLogs && (0, jsx_runtime_1.jsx)(remote_logs_panel_1.RemoteLogsPanel, { deviceId: device.deviceId }), device.sharedGroups.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "border-bottom pb-2 mb-3", children: ["Shared Groups (", device.sharedGroups.length, ")"] }), (0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-sm table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "Group ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Role" }), (0, jsx_runtime_1.jsx)("th", { children: "Sync Status" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: device.sharedGroups.map((groupId) => {
90
92
  const syncStats = device.syncStatsByGroup[groupId];
91
93
  const role = device.roles[groupId];
92
94
  return ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: groupId }) }), (0, jsx_runtime_1.jsx)("td", { children: role != null ? String(role) : "N/A" }), (0, jsx_runtime_1.jsx)("td", { children: syncStats ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [syncStats.isPreferred && ((0, jsx_runtime_1.jsx)("span", { className: "badge bg-primary me-2", children: "Preferred" })), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: new Date(syncStats.lastSyncTime).toLocaleString() })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "-" })) })] }, groupId));
@@ -1,3 +1,21 @@
1
+ import type { IConsoleLog } from "@peers-app/peers-sdk";
2
+ /** Filters and bounds accepted by the desktop remote-log diagnostic endpoint. */
3
+ export type RemoteLogsQuery = {
4
+ sinceTimestamp?: number;
5
+ level?: IConsoleLog["level"];
6
+ process?: string;
7
+ search?: string;
8
+ pageSize?: number;
9
+ timeoutMs?: number;
10
+ };
11
+ /** Result of reading a directly connected, provider-authorized device's console-log table. */
12
+ export type RemoteLogsResult = {
13
+ deviceId: string;
14
+ truncated: boolean;
15
+ logs: IConsoleLog[];
16
+ queriedAt: number;
17
+ };
18
+ /** Electron preload APIs used by the Network Viewer screen. */
1
19
  export type NetworkViewerElectronApi = {
2
20
  getOverview: () => Promise<unknown>;
3
21
  getConnections: () => Promise<unknown>;
@@ -16,7 +34,9 @@ export type NetworkViewerElectronApi = {
16
34
  }>;
17
35
  getDeviceDetails: (deviceId: string) => Promise<unknown>;
18
36
  getGroupDetails: (groupId: string) => Promise<unknown>;
37
+ getRemoteLogs: (deviceId: string, query?: RemoteLogsQuery) => Promise<RemoteLogsResult>;
19
38
  downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
20
39
  runDiagnostics: () => Promise<unknown>;
21
40
  };
41
+ /** Return the context-isolated desktop diagnostics bridge when running in Electron. */
22
42
  export declare function getNetworkViewerApi(): NetworkViewerElectronApi | undefined;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getNetworkViewerApi = getNetworkViewerApi;
4
+ /** Return the context-isolated desktop diagnostics bridge when running in Electron. */
4
5
  function getNetworkViewerApi() {
5
6
  return window.electronAPI?.networkViewer;
6
7
  }
@@ -0,0 +1,6 @@
1
+ interface RemoteLogsPanelProps {
2
+ deviceId: string;
3
+ }
4
+ /** Read-only System Logs contract panel for a directly connected, provider-authorized device. */
5
+ export declare function RemoteLogsPanel({ deviceId }: RemoteLogsPanelProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteLogsPanel = RemoteLogsPanel;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const network_viewer_ipc_1 = require("./network-viewer-ipc");
7
+ function RemoteLogRow({ log }) {
8
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "border-bottom py-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex flex-wrap gap-2 align-items-center small", children: [(0, jsx_runtime_1.jsx)("code", { children: new Date(log.timestamp).toLocaleTimeString() }), (0, jsx_runtime_1.jsx)("span", { className: `badge ${log.level === "error" ? "bg-danger" : "bg-secondary"}`, children: log.level }), (0, jsx_runtime_1.jsx)("span", { className: "badge bg-info text-dark", children: log.process }), log.source && (0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: log.source })] }), (0, jsx_runtime_1.jsx)("div", { className: "mt-1", style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: log.message }), (log.context || log.stackTrace) && ((0, jsx_runtime_1.jsxs)("details", { className: "mt-1", children: [(0, jsx_runtime_1.jsx)("summary", { className: "small text-muted", children: "Details" }), (0, jsx_runtime_1.jsx)("pre", { className: "small bg-body-tertiary rounded p-2 mt-1", style: { maxHeight: "240px" }, children: JSON.stringify({ context: log.context, stackTrace: log.stackTrace }, null, 2) })] }))] }));
9
+ }
10
+ /** Read-only System Logs contract panel for a directly connected, provider-authorized device. */
11
+ function RemoteLogsPanel({ deviceId }) {
12
+ const [result, setResult] = (0, react_1.useState)(null);
13
+ const [loading, setLoading] = (0, react_1.useState)(false);
14
+ const [error, setError] = (0, react_1.useState)();
15
+ const [processName, setProcessName] = (0, react_1.useState)("");
16
+ const [level, setLevel] = (0, react_1.useState)("");
17
+ const [search, setSearch] = (0, react_1.useState)("");
18
+ const [minutes, setMinutes] = (0, react_1.useState)("60");
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
+ setLoading(true);
26
+ setError(undefined);
27
+ try {
28
+ const minutesNumber = Number(minutes);
29
+ const sinceTimestamp = Number.isFinite(minutesNumber) && minutesNumber > 0
30
+ ? Date.now() - minutesNumber * 60_000
31
+ : 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,
39
+ });
40
+ setResult(response);
41
+ }
42
+ catch (cause) {
43
+ setError(cause instanceof Error ? cause.message : String(cause));
44
+ }
45
+ finally {
46
+ setLoading(false);
47
+ }
48
+ }, [deviceId, level, minutes, processName, search]);
49
+ const exportLogs = (0, react_1.useCallback)(() => {
50
+ if (!result)
51
+ return;
52
+ const blob = new Blob([JSON.stringify(result, null, 2)], { type: "application/json" });
53
+ const url = URL.createObjectURL(blob);
54
+ const anchor = document.createElement("a");
55
+ anchor.href = url;
56
+ anchor.download = `remote-logs-${deviceId}-${result.queriedAt}.json`;
57
+ anchor.click();
58
+ URL.revokeObjectURL(url);
59
+ }, [deviceId, result]);
60
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center border-bottom pb-2 mb-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "mb-0", children: "Remote Console Logs" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", onClick: exportLogs, disabled: !result, children: "Export JSON" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-primary", onClick: () => void loadLogs(), disabled: loading, children: loading ? "Loading..." : result ? "Refresh" : "Load logs" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "row g-2 mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-3", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small", htmlFor: "remote-logs-process", children: "Process" }), (0, jsx_runtime_1.jsx)("input", { id: "remote-logs-process", className: "form-control form-control-sm", value: processName, onChange: (event) => setProcessName(event.target.value), placeholder: "All processes" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small", htmlFor: "remote-logs-level", children: "Level" }), (0, jsx_runtime_1.jsxs)("select", { id: "remote-logs-level", className: "form-select form-select-sm", value: level, onChange: (event) => setLevel(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "All" }), (0, jsx_runtime_1.jsx)("option", { value: "debug", children: "Debug" }), (0, jsx_runtime_1.jsx)("option", { value: "info", children: "Info" }), (0, jsx_runtime_1.jsx)("option", { value: "log", children: "Log" }), (0, jsx_runtime_1.jsx)("option", { value: "warn", children: "Warn" }), (0, jsx_runtime_1.jsx)("option", { value: "error", children: "Error" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small", htmlFor: "remote-logs-minutes", children: "Time range" }), (0, jsx_runtime_1.jsxs)("select", { id: "remote-logs-minutes", className: "form-select form-select-sm", value: minutes, onChange: (event) => setMinutes(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "15", children: "15 minutes" }), (0, jsx_runtime_1.jsx)("option", { value: "60", children: "1 hour" }), (0, jsx_runtime_1.jsx)("option", { value: "360", children: "6 hours" }), (0, jsx_runtime_1.jsx)("option", { value: "0", children: "All retained" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-5", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small", htmlFor: "remote-logs-search", children: "Message words" }), (0, jsx_runtime_1.jsx)("input", { id: "remote-logs-search", className: "form-control form-control-sm", value: search, onChange: (event) => setSearch(event.target.value), placeholder: "BrowserWebRTC" })] })] }), error && (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }), result && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "small text-muted mb-2", children: ["Showing ", result.logs.length, result.truncated ? " newest" : "", " matching logs", result.truncated ? "; narrow the filters to see a different slice" : "", "; queried", " ", new Date(result.queriedAt).toLocaleString(), "."] }), (0, jsx_runtime_1.jsx)("div", { className: "border rounded px-2", style: { maxHeight: "420px", overflowY: "auto" }, children: result.logs.length ? (result.logs.map((log) => (0, jsx_runtime_1.jsx)(RemoteLogRow, { log: log }, log.logId))) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted py-3 text-center", children: "No matching remote logs." })) })] }))] }));
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.19.13",
3
+ "version": "0.20.0",
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.19.13",
31
+ "@peers-app/peers-sdk": "^0.20.0",
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-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.19.13",
42
+ "@peers-app/peers-sdk": "0.20.0",
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",
@@ -15,6 +15,7 @@ interface TrustLevelDropdownProps {
15
15
  type?: "user" | "device";
16
16
  }
17
17
 
18
+ /** Select a context-scoped user or device trust level, including the full-access Self grant. */
18
19
  export const TrustLevelDropdown = ({
19
20
  userId,
20
21
  dataContext,
@@ -25,7 +26,12 @@ export const TrustLevelDropdown = ({
25
26
  const [loading, setLoading] = useState(true);
26
27
 
27
28
  const allTrustLevelConfigs = [
28
- { value: TrustLevel.Self, color: "success", icon: "bi-shield-fill-check", text: "Self" },
29
+ {
30
+ value: TrustLevel.Self,
31
+ color: "success",
32
+ icon: "bi-shield-fill-check",
33
+ text: "Self (full remote contract access)",
34
+ },
29
35
  { value: TrustLevel.Trusted, color: "success", icon: "bi-shield-check", text: "Trusted" },
30
36
  { value: TrustLevel.Known, color: "primary", icon: "bi-shield-check", text: "Known" },
31
37
  {
@@ -12,6 +12,7 @@ interface IProps {
12
12
  userId: string;
13
13
  }
14
14
 
15
+ /** Edit a contact's signed profile fields and personal-context trust assignment. */
15
16
  export const ContactDetails = (props: IProps) => {
16
17
  const contact = usePromise(
17
18
  async () => {
@@ -94,6 +95,9 @@ const ContactInfo = (props: { contact: IDoc<IUser> }) => {
94
95
  <div className="border-start ps-3">
95
96
  <small className="text-muted d-block mb-1">Trust Level</small>
96
97
  <TrustLevelDropdown userId={contact.userId} />
98
+ <small className="text-muted d-block mt-1">
99
+ Self grants this user full remote contract access.
100
+ </small>
97
101
  </div>
98
102
  </div>
99
103
  </div>
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react";
3
3
  import { LoadingIndicator } from "../../components/loading-indicator";
4
4
  import { TrustLevelBadge } from "../../components/trust-level-badge";
5
5
  import { getNetworkViewerApi } from "./network-viewer-ipc";
6
+ import { RemoteLogsPanel } from "./remote-logs-panel";
6
7
 
7
8
  /** Format bytes to human-readable string (KB, MB, GB) */
8
9
  function formatBytes(bytes: number): string {
@@ -31,6 +32,7 @@ interface IDeviceDetails {
31
32
  deviceName?: string;
32
33
  trustLevel: number;
33
34
  connectionState: "connected" | "syncing" | "idle";
35
+ canReadRemoteLogs: boolean;
34
36
  sharedGroups: string[];
35
37
  roles: Record<string, unknown>;
36
38
  pathType: "direct" | "indirect" | "unknown";
@@ -63,6 +65,7 @@ interface IDeviceDetailsModalProps {
63
65
  onDisconnect?: () => void;
64
66
  }
65
67
 
68
+ /** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
66
69
  export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceDetailsModalProps) {
67
70
  const [device, setDevice] = useState<IDeviceDetails | null>(null);
68
71
  const [loading, setLoading] = useState(true);
@@ -244,6 +247,8 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
244
247
  </div>
245
248
  </div>
246
249
 
250
+ {device.canReadRemoteLogs && <RemoteLogsPanel deviceId={device.deviceId} />}
251
+
247
252
  {/* Shared Groups */}
248
253
  {device.sharedGroups.length > 0 && (
249
254
  <div className="mb-4">
@@ -1,3 +1,24 @@
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
+ };
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
+ };
20
+
21
+ /** Electron preload APIs used by the Network Viewer screen. */
1
22
  export type NetworkViewerElectronApi = {
2
23
  getOverview: () => Promise<unknown>;
3
24
  getConnections: () => Promise<unknown>;
@@ -10,6 +31,7 @@ export type NetworkViewerElectronApi = {
10
31
  deleteDevice: (deviceId: string) => Promise<{ success: boolean; message?: string }>;
11
32
  getDeviceDetails: (deviceId: string) => Promise<unknown>;
12
33
  getGroupDetails: (groupId: string) => Promise<unknown>;
34
+ getRemoteLogs: (deviceId: string, query?: RemoteLogsQuery) => Promise<RemoteLogsResult>;
13
35
  downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
14
36
  runDiagnostics: () => Promise<unknown>;
15
37
  };
@@ -18,6 +40,7 @@ type WindowWithNetworkViewer = Window & {
18
40
  electronAPI?: { networkViewer?: NetworkViewerElectronApi };
19
41
  };
20
42
 
43
+ /** Return the context-isolated desktop diagnostics bridge when running in Electron. */
21
44
  export function getNetworkViewerApi(): NetworkViewerElectronApi | undefined {
22
45
  return (window as WindowWithNetworkViewer).electronAPI?.networkViewer;
23
46
  }
@@ -0,0 +1,192 @@
1
+ import type { IConsoleLog } from "@peers-app/peers-sdk";
2
+ import { useCallback, useState } from "react";
3
+ import { getNetworkViewerApi, type RemoteLogsResult } from "./network-viewer-ipc";
4
+
5
+ interface RemoteLogsPanelProps {
6
+ deviceId: string;
7
+ }
8
+
9
+ function RemoteLogRow({ log }: { log: IConsoleLog }) {
10
+ return (
11
+ <div className="border-bottom py-2">
12
+ <div className="d-flex flex-wrap gap-2 align-items-center small">
13
+ <code>{new Date(log.timestamp).toLocaleTimeString()}</code>
14
+ <span className={`badge ${log.level === "error" ? "bg-danger" : "bg-secondary"}`}>
15
+ {log.level}
16
+ </span>
17
+ <span className="badge bg-info text-dark">{log.process}</span>
18
+ {log.source && <span className="text-muted">{log.source}</span>}
19
+ </div>
20
+ <div className="mt-1" style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
21
+ {log.message}
22
+ </div>
23
+ {(log.context || log.stackTrace) && (
24
+ <details className="mt-1">
25
+ <summary className="small text-muted">Details</summary>
26
+ <pre className="small bg-body-tertiary rounded p-2 mt-1" style={{ maxHeight: "240px" }}>
27
+ {JSON.stringify({ context: log.context, stackTrace: log.stackTrace }, null, 2)}
28
+ </pre>
29
+ </details>
30
+ )}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ /** Read-only System Logs contract panel for a directly connected, provider-authorized device. */
36
+ export function RemoteLogsPanel({ deviceId }: RemoteLogsPanelProps) {
37
+ const [result, setResult] = useState<RemoteLogsResult | null>(null);
38
+ const [loading, setLoading] = useState(false);
39
+ const [error, setError] = useState<string>();
40
+ const [processName, setProcessName] = useState("");
41
+ const [level, setLevel] = useState("");
42
+ const [search, setSearch] = useState("");
43
+ const [minutes, setMinutes] = useState("60");
44
+
45
+ const loadLogs = useCallback(async () => {
46
+ const api = getNetworkViewerApi();
47
+ if (!api) {
48
+ setError("Network Viewer API is unavailable");
49
+ return;
50
+ }
51
+
52
+ setLoading(true);
53
+ setError(undefined);
54
+ try {
55
+ const minutesNumber = Number(minutes);
56
+ const sinceTimestamp =
57
+ Number.isFinite(minutesNumber) && minutesNumber > 0
58
+ ? Date.now() - minutesNumber * 60_000
59
+ : 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,
67
+ });
68
+ setResult(response);
69
+ } catch (cause) {
70
+ setError(cause instanceof Error ? cause.message : String(cause));
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ }, [deviceId, level, minutes, processName, search]);
75
+
76
+ const exportLogs = useCallback(() => {
77
+ if (!result) return;
78
+ const blob = new Blob([JSON.stringify(result, null, 2)], { type: "application/json" });
79
+ const url = URL.createObjectURL(blob);
80
+ const anchor = document.createElement("a");
81
+ anchor.href = url;
82
+ anchor.download = `remote-logs-${deviceId}-${result.queriedAt}.json`;
83
+ anchor.click();
84
+ URL.revokeObjectURL(url);
85
+ }, [deviceId, result]);
86
+
87
+ return (
88
+ <div className="mb-4">
89
+ <div className="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
90
+ <h6 className="mb-0">Remote Console Logs</h6>
91
+ <div className="d-flex gap-2">
92
+ <button
93
+ type="button"
94
+ className="btn btn-sm btn-outline-secondary"
95
+ onClick={exportLogs}
96
+ disabled={!result}
97
+ >
98
+ Export JSON
99
+ </button>
100
+ <button
101
+ type="button"
102
+ className="btn btn-sm btn-primary"
103
+ onClick={() => void loadLogs()}
104
+ disabled={loading}
105
+ >
106
+ {loading ? "Loading..." : result ? "Refresh" : "Load logs"}
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <div className="row g-2 mb-3">
112
+ <div className="col-md-3">
113
+ <label className="form-label small" htmlFor="remote-logs-process">
114
+ Process
115
+ </label>
116
+ <input
117
+ id="remote-logs-process"
118
+ className="form-control form-control-sm"
119
+ value={processName}
120
+ onChange={(event) => setProcessName(event.target.value)}
121
+ placeholder="All processes"
122
+ />
123
+ </div>
124
+ <div className="col-md-2">
125
+ <label className="form-label small" htmlFor="remote-logs-level">
126
+ Level
127
+ </label>
128
+ <select
129
+ id="remote-logs-level"
130
+ className="form-select form-select-sm"
131
+ value={level}
132
+ onChange={(event) => setLevel(event.target.value)}
133
+ >
134
+ <option value="">All</option>
135
+ <option value="debug">Debug</option>
136
+ <option value="info">Info</option>
137
+ <option value="log">Log</option>
138
+ <option value="warn">Warn</option>
139
+ <option value="error">Error</option>
140
+ </select>
141
+ </div>
142
+ <div className="col-md-2">
143
+ <label className="form-label small" htmlFor="remote-logs-minutes">
144
+ Time range
145
+ </label>
146
+ <select
147
+ id="remote-logs-minutes"
148
+ className="form-select form-select-sm"
149
+ value={minutes}
150
+ onChange={(event) => setMinutes(event.target.value)}
151
+ >
152
+ <option value="15">15 minutes</option>
153
+ <option value="60">1 hour</option>
154
+ <option value="360">6 hours</option>
155
+ <option value="0">All retained</option>
156
+ </select>
157
+ </div>
158
+ <div className="col-md-5">
159
+ <label className="form-label small" htmlFor="remote-logs-search">
160
+ Message words
161
+ </label>
162
+ <input
163
+ id="remote-logs-search"
164
+ className="form-control form-control-sm"
165
+ value={search}
166
+ onChange={(event) => setSearch(event.target.value)}
167
+ placeholder="BrowserWebRTC"
168
+ />
169
+ </div>
170
+ </div>
171
+
172
+ {error && <div className="alert alert-danger py-2">{error}</div>}
173
+ {result && (
174
+ <>
175
+ <div className="small text-muted mb-2">
176
+ Showing {result.logs.length}
177
+ {result.truncated ? " newest" : ""} matching logs
178
+ {result.truncated ? "; narrow the filters to see a different slice" : ""}; queried{" "}
179
+ {new Date(result.queriedAt).toLocaleString()}.
180
+ </div>
181
+ <div className="border rounded px-2" style={{ maxHeight: "420px", overflowY: "auto" }}>
182
+ {result.logs.length ? (
183
+ result.logs.map((log) => <RemoteLogRow key={log.logId} log={log} />)
184
+ ) : (
185
+ <div className="text-muted py-3 text-center">No matching remote logs.</div>
186
+ )}
187
+ </div>
188
+ </>
189
+ )}
190
+ </div>
191
+ );
192
+ }