sysgraph 0.0.17__tar.gz → 0.0.18__tar.gz
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.
- {sysgraph-0.0.17/src/sysgraph.egg-info → sysgraph-0.0.18}/PKG-INFO +17 -14
- {sysgraph-0.0.17 → sysgraph-0.0.18}/README.md +13 -12
- {sysgraph-0.0.17 → sysgraph-0.0.18}/pyproject.toml +3 -1
- sysgraph-0.0.18/src/sysgraph/__init__.py +1 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/app.py +6 -1
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/constants.py +2 -2
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/discovery.py +41 -12
- sysgraph-0.0.18/src/sysgraph/dist/assets/index-BLbtXZbQ.css +1 -0
- sysgraph-0.0.17/src/sysgraph/dist/assets/index-BlTIaOJH.js → sysgraph-0.0.18/src/sysgraph/dist/assets/index-Dq5MCDEE.js +98 -98
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/dist/index.html +6 -4
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/main.py +4 -1
- {sysgraph-0.0.17 → sysgraph-0.0.18/src/sysgraph.egg-info}/PKG-INFO +17 -14
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/SOURCES.txt +2 -2
- sysgraph-0.0.17/src/sysgraph/__init__.py +0 -1
- sysgraph-0.0.17/src/sysgraph/dist/assets/index-BBuhcA5M.css +0 -1
- {sysgraph-0.0.17 → sysgraph-0.0.18}/LICENSE +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/MANIFEST.in +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/setup.cfg +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/__main__.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/dist/assets/icon-TKtfQOgj.png +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/graph.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/model.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/compute_vc.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/convert_to_graph_format.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/histogram_weights.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/viral_centrality.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/voles_network/convert_voles_to_graph_format.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/tests/test_discovery.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/tests/test_graph.py +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/dependency_links.txt +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/entry_points.txt +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/requires.txt +0 -0
- {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/top_level.txt +0 -0
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sysgraph
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.18
|
|
4
4
|
Summary: Visualizer for processes and their interconnections
|
|
5
5
|
Author-email: Eugene Gubenkov <gubenkoved@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Source, https://github.com/gubenkoved/sysgraph
|
|
8
8
|
Project-URL: Issues, https://github.com/gubenkoved/sysgraph/issues
|
|
9
|
-
Keywords: process,visualization,graph,ipc,system,monitoring,linux
|
|
9
|
+
Keywords: process,visualization,graph,ipc,system,monitoring,cross-platform,macos,linux
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
17
|
Classifier: Topic :: System :: Monitoring
|
|
16
18
|
Classifier: Topic :: System :: Systems Administration
|
|
17
19
|
Classifier: Environment :: Web Environment
|
|
@@ -31,9 +33,10 @@ Dynamic: license-file
|
|
|
31
33
|
|
|
32
34
|
An interactive force-directed **network graph visualizer** for the browser — with two modes:
|
|
33
35
|
|
|
34
|
-
- **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
|
|
35
|
-
- **Live process graph** — discover running OS processes and their inter-process communication channels
|
|
36
|
+
- **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
|
|
37
|
+
- **Live process graph** — discover running OS processes and their inter-process communication channels in real time (cross-platform; richest on Linux)
|
|
36
38
|
|
|
39
|
+
[](https://pypi.org/project/sysgraph/)
|
|
37
40
|

|
|
38
41
|

|
|
39
42
|
|
|
@@ -45,9 +48,9 @@ An interactive force-directed **network graph visualizer** for the browser — w
|
|
|
45
48
|
- **Fuzzy search** — find nodes by any property
|
|
46
49
|
- **Adjacency filtering** — right-click a node to show only its neighbors
|
|
47
50
|
- **Configurable** — tune d3 force parameters, colors, and type filters via the settings panel
|
|
48
|
-
- **Process discovery**
|
|
49
|
-
- **IPC visualization**
|
|
50
|
-
- **Real-time**
|
|
51
|
+
- **Process discovery** — enumerates running OS processes and their parent-child relationships (cross-platform via psutil)
|
|
52
|
+
- **IPC visualization** — discovers TCP/UDP connections (all platforms), Unix domain sockets and pipes (Linux only)
|
|
53
|
+
- **Real-time** — fetch the latest process graph on demand via the web UI
|
|
51
54
|
|
|
52
55
|
## Demo
|
|
53
56
|
|
|
@@ -56,8 +59,8 @@ An interactive force-directed **network graph visualizer** for the browser — w
|
|
|
56
59
|
## Requirements
|
|
57
60
|
|
|
58
61
|
- **Python ≥ 3.12**
|
|
59
|
-
- **Linux
|
|
60
|
-
- Root/sudo recommended for full process visibility
|
|
62
|
+
- **Linux, macOS, or Windows** — process and network discovery works on all platforms via psutil; Unix domain sockets and pipe discovery require Linux
|
|
63
|
+
- Root/sudo recommended for full process visibility on Linux/macOS
|
|
61
64
|
|
|
62
65
|
## Installation
|
|
63
66
|
|
|
@@ -98,9 +101,9 @@ Use the **Import** button in the UI to load any JSON file in the following forma
|
|
|
98
101
|
|
|
99
102
|
See [`data/simplest-graph.json`](data/simplest-graph.json) for a minimal example.
|
|
100
103
|
|
|
101
|
-
### Live process graph
|
|
104
|
+
### Live process graph
|
|
102
105
|
|
|
103
|
-
For full visibility into all processes and their connections, run with elevated privileges:
|
|
106
|
+
For full visibility into all processes and their connections, run with elevated privileges (Linux/macOS):
|
|
104
107
|
|
|
105
108
|
```bash
|
|
106
109
|
sudo sysgraph
|
|
@@ -118,12 +121,12 @@ The `--pid=host` and `--net=host` flags allow the container to see host processe
|
|
|
118
121
|
|
|
119
122
|
1. The **browser frontend** renders interactive force-directed graphs using [force-graph](https://github.com/vasturiano/force-graph) with d3 physics simulation.
|
|
120
123
|
2. Graphs can be **imported from JSON** directly in the browser, or fetched live from the backend.
|
|
121
|
-
3. The **FastAPI backend** uses `psutil` and Linux-specific APIs (`/proc`, `ss`)
|
|
124
|
+
3. The **FastAPI backend** uses `psutil` to discover processes and network connections (cross-platform), plus Linux-specific APIs (`/proc`, `ss`) for pipe and Unix domain socket discovery, building a graph served via `GET /api/graph`.
|
|
122
125
|
|
|
123
126
|
## Development
|
|
124
127
|
|
|
125
128
|
### Prerequisites
|
|
126
|
-
- Python ≥ 3.12,
|
|
129
|
+
- Python ≥ 3.12, Docker (for frontend builds)
|
|
127
130
|
- Node.js 22 runs inside Docker; no host installation required
|
|
128
131
|
|
|
129
132
|
### Backend
|
|
@@ -143,7 +146,7 @@ python src/sysgraph/app.py # → http://localhost:8000
|
|
|
143
146
|
|
|
144
147
|
### Tests
|
|
145
148
|
```bash
|
|
146
|
-
pytest src/sysgraph/tests/
|
|
149
|
+
pytest src/sysgraph/tests/
|
|
147
150
|
```
|
|
148
151
|
|
|
149
152
|
### Python linting
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
An interactive force-directed **network graph visualizer** for the browser — with two modes:
|
|
4
4
|
|
|
5
|
-
- **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
|
|
6
|
-
- **Live process graph** — discover running OS processes and their inter-process communication channels
|
|
5
|
+
- **Import any graph** — load any JSON graph (nodes + edges) to explore and visualize it interactively
|
|
6
|
+
- **Live process graph** — discover running OS processes and their inter-process communication channels in real time (cross-platform; richest on Linux)
|
|
7
7
|
|
|
8
|
+
[](https://pypi.org/project/sysgraph/)
|
|
8
9
|

|
|
9
10
|

|
|
10
11
|
|
|
@@ -16,9 +17,9 @@ An interactive force-directed **network graph visualizer** for the browser — w
|
|
|
16
17
|
- **Fuzzy search** — find nodes by any property
|
|
17
18
|
- **Adjacency filtering** — right-click a node to show only its neighbors
|
|
18
19
|
- **Configurable** — tune d3 force parameters, colors, and type filters via the settings panel
|
|
19
|
-
- **Process discovery**
|
|
20
|
-
- **IPC visualization**
|
|
21
|
-
- **Real-time**
|
|
20
|
+
- **Process discovery** — enumerates running OS processes and their parent-child relationships (cross-platform via psutil)
|
|
21
|
+
- **IPC visualization** — discovers TCP/UDP connections (all platforms), Unix domain sockets and pipes (Linux only)
|
|
22
|
+
- **Real-time** — fetch the latest process graph on demand via the web UI
|
|
22
23
|
|
|
23
24
|
## Demo
|
|
24
25
|
|
|
@@ -27,8 +28,8 @@ An interactive force-directed **network graph visualizer** for the browser — w
|
|
|
27
28
|
## Requirements
|
|
28
29
|
|
|
29
30
|
- **Python ≥ 3.12**
|
|
30
|
-
- **Linux
|
|
31
|
-
- Root/sudo recommended for full process visibility
|
|
31
|
+
- **Linux, macOS, or Windows** — process and network discovery works on all platforms via psutil; Unix domain sockets and pipe discovery require Linux
|
|
32
|
+
- Root/sudo recommended for full process visibility on Linux/macOS
|
|
32
33
|
|
|
33
34
|
## Installation
|
|
34
35
|
|
|
@@ -69,9 +70,9 @@ Use the **Import** button in the UI to load any JSON file in the following forma
|
|
|
69
70
|
|
|
70
71
|
See [`data/simplest-graph.json`](data/simplest-graph.json) for a minimal example.
|
|
71
72
|
|
|
72
|
-
### Live process graph
|
|
73
|
+
### Live process graph
|
|
73
74
|
|
|
74
|
-
For full visibility into all processes and their connections, run with elevated privileges:
|
|
75
|
+
For full visibility into all processes and their connections, run with elevated privileges (Linux/macOS):
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
78
|
sudo sysgraph
|
|
@@ -89,12 +90,12 @@ The `--pid=host` and `--net=host` flags allow the container to see host processe
|
|
|
89
90
|
|
|
90
91
|
1. The **browser frontend** renders interactive force-directed graphs using [force-graph](https://github.com/vasturiano/force-graph) with d3 physics simulation.
|
|
91
92
|
2. Graphs can be **imported from JSON** directly in the browser, or fetched live from the backend.
|
|
92
|
-
3. The **FastAPI backend** uses `psutil` and Linux-specific APIs (`/proc`, `ss`)
|
|
93
|
+
3. The **FastAPI backend** uses `psutil` to discover processes and network connections (cross-platform), plus Linux-specific APIs (`/proc`, `ss`) for pipe and Unix domain socket discovery, building a graph served via `GET /api/graph`.
|
|
93
94
|
|
|
94
95
|
## Development
|
|
95
96
|
|
|
96
97
|
### Prerequisites
|
|
97
|
-
- Python ≥ 3.12,
|
|
98
|
+
- Python ≥ 3.12, Docker (for frontend builds)
|
|
98
99
|
- Node.js 22 runs inside Docker; no host installation required
|
|
99
100
|
|
|
100
101
|
### Backend
|
|
@@ -114,7 +115,7 @@ python src/sysgraph/app.py # → http://localhost:8000
|
|
|
114
115
|
|
|
115
116
|
### Tests
|
|
116
117
|
```bash
|
|
117
|
-
pytest src/sysgraph/tests/
|
|
118
|
+
pytest src/sysgraph/tests/
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
### Python linting
|
|
@@ -10,13 +10,15 @@ readme = "README.md"
|
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
authors = [{ name = "Eugene Gubenkov", email = "gubenkoved@gmail.com" }]
|
|
12
12
|
requires-python = ">=3.12"
|
|
13
|
-
keywords = ["process", "visualization", "graph", "ipc", "system", "monitoring", "
|
|
13
|
+
keywords = ["process", "visualization", "graph", "ipc", "system", "monitoring", "cross-platform", "macos", "linux"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
17
17
|
"Programming Language :: Python :: 3.13",
|
|
18
18
|
"License :: OSI Approved :: MIT License",
|
|
19
19
|
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
22
|
"Topic :: System :: Monitoring",
|
|
21
23
|
"Topic :: System :: Systems Administration",
|
|
22
24
|
"Environment :: Web Environment",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.18"
|
|
@@ -18,9 +18,14 @@ from sysgraph.discovery import build_graph
|
|
|
18
18
|
LOGGER = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
LOG_FORMAT = (
|
|
22
|
+
"%(asctime)s.%(msecs)03d %(name)s[%(process)d] %(levelname)s %(message)s"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
21
26
|
@asynccontextmanager
|
|
22
27
|
async def lifespan(app):
|
|
23
|
-
coloredlogs.install()
|
|
28
|
+
coloredlogs.install(fmt=LOG_FORMAT)
|
|
24
29
|
logging.info(f"init for process with PID {os.getpid()}")
|
|
25
30
|
yield
|
|
26
31
|
logging.info(f"shutdown for process with PID {os.getpid()}")
|
|
@@ -20,8 +20,8 @@ def process_node_id(pid: int) -> str:
|
|
|
20
20
|
return f"{NODE_PROCESS}::{pid}"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def uds_node_id(
|
|
24
|
-
return f"{NODE_UDS}::{
|
|
23
|
+
def uds_node_id(key: str) -> str:
|
|
24
|
+
return f"{NODE_UDS}::{key}"
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def pipe_node_id(inode: str) -> str:
|
|
@@ -2,6 +2,8 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
5
7
|
from collections import defaultdict
|
|
6
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
9
|
from pathlib import Path
|
|
@@ -71,7 +73,7 @@ def discover_processes() -> list[Process]:
|
|
|
71
73
|
if mem_info:
|
|
72
74
|
p.memory_rss = mem_info.rss
|
|
73
75
|
p.memory_vms = mem_info.vms
|
|
74
|
-
p.memory_shared = mem_info
|
|
76
|
+
p.memory_shared = getattr(mem_info, "shared", None)
|
|
75
77
|
|
|
76
78
|
p.environment = info.get("environ")
|
|
77
79
|
|
|
@@ -81,15 +83,18 @@ def discover_processes() -> list[Process]:
|
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def discover_unix_sockets() -> list[UnixDomainSocket]:
|
|
84
|
-
"""Discover Unix Domain Sockets
|
|
86
|
+
"""Discover Unix Domain Sockets by parsing ``ss -xp`` output.
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
find connected pairs of processes.
|
|
88
|
+
This is a Linux-only feature. On other platforms an empty list is
|
|
89
|
+
returned without raising an error.
|
|
89
90
|
|
|
90
91
|
Returns:
|
|
91
|
-
list[UnixDomainSocket]:
|
|
92
|
+
list[UnixDomainSocket]: Discovered UDS sockets.
|
|
92
93
|
"""
|
|
94
|
+
if sys.platform != "linux":
|
|
95
|
+
LOGGER.debug("UDS discovery skipped (only supported on Linux)")
|
|
96
|
+
return []
|
|
97
|
+
|
|
93
98
|
result = subprocess.run(
|
|
94
99
|
["ss", "-xp"], capture_output=True, text=True, check=False
|
|
95
100
|
)
|
|
@@ -98,11 +103,15 @@ def discover_unix_sockets() -> list[UnixDomainSocket]:
|
|
|
98
103
|
|
|
99
104
|
# example line:
|
|
100
105
|
# users:(("dbus-daemon",pid=1950,fd=12))
|
|
101
|
-
def parse_proccess(
|
|
106
|
+
def parse_proccess(
|
|
107
|
+
s: str,
|
|
108
|
+
) -> list[UnixDomainSocketProcRef]:
|
|
102
109
|
processes = []
|
|
103
110
|
|
|
104
111
|
for match in re.finditer(
|
|
105
|
-
r'\("(?P<name>[^"]+)",pid=(?P<pid>[0-9]+),
|
|
112
|
+
r'\("(?P<name>[^"]+)",pid=(?P<pid>[0-9]+),'
|
|
113
|
+
r"fd=(?P<fd>[0-9]+)\)",
|
|
114
|
+
s,
|
|
106
115
|
):
|
|
107
116
|
ref = UnixDomainSocketProcRef(pid=int(match.group("pid")))
|
|
108
117
|
ref.name = match.group("name")
|
|
@@ -140,7 +149,8 @@ def discover_connected_uds(
|
|
|
140
149
|
) -> list[UnixDomainSocketConnection]:
|
|
141
150
|
"""Discover connected UDS pairs from the list of discovered UDS sockets.
|
|
142
151
|
|
|
143
|
-
Each connection will have two UDS sockets with opposite local/peer
|
|
152
|
+
Each connection will have two UDS sockets with opposite local/peer
|
|
153
|
+
inodes.
|
|
144
154
|
|
|
145
155
|
Args:
|
|
146
156
|
sockets (list[UnixDomainSocket]): List of discovered UDS sockets.
|
|
@@ -158,7 +168,10 @@ def discover_connected_uds(
|
|
|
158
168
|
for uds in sockets:
|
|
159
169
|
if (uds.peer_inode, uds.local_inode) in inode_map:
|
|
160
170
|
peer_uds = inode_map[(uds.peer_inode, uds.local_inode)]
|
|
161
|
-
if (
|
|
171
|
+
if (
|
|
172
|
+
uds.local_inode,
|
|
173
|
+
uds.peer_inode,
|
|
174
|
+
) not in visited and (
|
|
162
175
|
uds.peer_inode,
|
|
163
176
|
uds.local_inode,
|
|
164
177
|
) not in visited:
|
|
@@ -174,8 +187,18 @@ def discover_connected_uds(
|
|
|
174
187
|
|
|
175
188
|
|
|
176
189
|
def get_processes_open_files() -> dict[int, list[ProcessOpenFile]]:
|
|
177
|
-
"""Read open file descriptors
|
|
190
|
+
"""Read open file descriptors from /proc to discover pipes.
|
|
191
|
+
|
|
192
|
+
This is a Linux-only feature that reads ``/proc/[pid]/fd`` and
|
|
193
|
+
``/proc/[pid]/fdinfo``. On other platforms an empty mapping is
|
|
194
|
+
returned without raising an error.
|
|
195
|
+
"""
|
|
178
196
|
result_map: dict[int, list[ProcessOpenFile]] = defaultdict(list)
|
|
197
|
+
|
|
198
|
+
if sys.platform != "linux":
|
|
199
|
+
LOGGER.debug("pipe discovery skipped (only supported on Linux)")
|
|
200
|
+
return result_map
|
|
201
|
+
|
|
179
202
|
proc_path = Path("/proc")
|
|
180
203
|
|
|
181
204
|
for pid_dir in proc_path.iterdir():
|
|
@@ -305,7 +328,7 @@ def _add_uds_nodes(
|
|
|
305
328
|
label = f"uds:[{inode}]"
|
|
306
329
|
|
|
307
330
|
node = graph.add_node(
|
|
308
|
-
uds_node_id(inode),
|
|
331
|
+
uds_node_id(str(inode)),
|
|
309
332
|
NODE_UDS,
|
|
310
333
|
properties={
|
|
311
334
|
"label": label,
|
|
@@ -513,6 +536,8 @@ def _add_network_nodes(
|
|
|
513
536
|
|
|
514
537
|
|
|
515
538
|
def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
539
|
+
started_at = time.monotonic()
|
|
540
|
+
|
|
516
541
|
graph = Graph()
|
|
517
542
|
|
|
518
543
|
# run independent discovery steps in parallel (all I/O-bound)
|
|
@@ -532,4 +557,8 @@ def build_graph(discover_uds_connectivity: bool = True) -> Graph:
|
|
|
532
557
|
_add_pipe_nodes(graph, open_files_map, pid_to_node)
|
|
533
558
|
_add_network_nodes(graph, processes, all_net_connections, pid_to_node)
|
|
534
559
|
|
|
560
|
+
LOGGER.info(
|
|
561
|
+
f"discovery completed in {time.monotonic() - started_at:.2f} seconds"
|
|
562
|
+
)
|
|
563
|
+
|
|
535
564
|
return graph
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--z-toolbar: 100;--z-search-help: 150;--z-context-menu: 200;--z-loading-overlay: 300;--z-details-panel: 50;--z-selection-overlay: 10;--z-version-badge: 10;--z-zoom-indicator: 10;--bg-toolbar: rgba(255, 255, 255, .95);--bg-panel: rgba(255, 255, 255, .97);--border-subtle: rgba(0, 0, 0, .08);--border-light: rgba(0, 0, 0, .1);--border-medium: rgba(0, 0, 0, .12);--shadow-xs: rgba(0, 0, 0, .08);--shadow-sm: rgba(0, 0, 0, .14);--shadow-md: rgba(0, 0, 0, .15);--accent-primary: #1a56db;--accent-info: #1a73e8;--accent-danger: #dc2626;--toolbar-height: 48px;--spacing-xs: 4px;--spacing-sm: 6px;--spacing-md: 8px;--spacing-lg: 16px;--font-family: "Ubuntu", "Segoe UI", "Arial", sans-serif;--font-size-xs: 11px;--font-size-sm: 12px;--font-size-md: 13px;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--button-height: 28px;--icon-button-size: 24px;--tp-base-background-color: hsla(230, 5%, 90%, 1);--tp-base-shadow-color: hsla(0, 0%, 0%, .1);--tp-button-background-color: hsla(230, 7%, 75%, 1);--tp-button-background-color-active: hsla(230, 7%, 60%, 1);--tp-button-background-color-focus: hsla(230, 7%, 65%, 1);--tp-button-background-color-hover: hsla(230, 7%, 70%, 1);--tp-button-foreground-color: hsla(230, 10%, 30%, 1);--tp-container-background-color: hsla(230, 15%, 30%, .2);--tp-container-background-color-active: hsla(230, 15%, 30%, .32);--tp-container-background-color-focus: hsla(230, 15%, 30%, .28);--tp-container-background-color-hover: hsla(230, 15%, 30%, .24);--tp-container-foreground-color: hsla(230, 10%, 30%, 1);--tp-groove-foreground-color: hsla(230, 15%, 30%, .1);--tp-input-background-color: hsla(230, 15%, 30%, .1);--tp-input-background-color-active: hsla(230, 15%, 30%, .22);--tp-input-background-color-focus: hsla(230, 15%, 30%, .18);--tp-input-background-color-hover: hsla(230, 15%, 30%, .14);--tp-input-foreground-color: hsla(230, 10%, 30%, 1);--tp-label-foreground-color: hsla(230, 10%, 30%, .7);--tp-monitor-background-color: hsla(230, 15%, 30%, .1);--tp-monitor-foreground-color: hsla(230, 10%, 30%, .5)}html,body{height:100%;margin:0;overflow:hidden;font-family:var(--font-family)}#toolbar{position:fixed;top:0;left:0;right:0;height:var(--toolbar-height);z-index:var(--z-toolbar);background:var(--bg-toolbar);border-bottom:1px solid var(--border-light);display:flex;align-items:center;padding:0 var(--spacing-lg);gap:var(--spacing-sm);box-shadow:0 1px 3px var(--shadow-xs)}#toolbar md-outlined-button{--md-outlined-button-container-height: var(--button-height);--md-outlined-button-label-text-size: var(--font-size-sm);--md-outlined-button-label-text-weight: 500;--md-outlined-button-icon-size: 16px;--md-outlined-button-leading-space: 10px;--md-outlined-button-trailing-space: 12px;--md-outlined-button-icon-offset: -2px;--md-outlined-button-container-shape: var(--radius-sm)}md-outlined-button.active{--md-outlined-button-container-color: #d0e1ff;--md-outlined-button-label-text-color: var(--accent-primary);--md-outlined-button-outline-color: var(--accent-primary);--md-outlined-button-icon-color: var(--accent-primary)}md-outlined-button.danger{--md-outlined-button-label-text-color: var(--accent-danger);--md-outlined-button-outline-color: var(--accent-danger);--md-outlined-button-icon-color: var(--accent-danger)}.toolbar-separator{width:1px;height:var(--icon-button-size);background:var(--border-light)}#searchInput{width:240px;height:var(--button-height);--md-outlined-text-field-container-shape: var(--radius-sm);--md-outlined-text-field-top-space: var(--spacing-xs);--md-outlined-text-field-bottom-space: var(--spacing-xs);--md-outlined-text-field-input-text-size: var(--font-size-sm);--md-outlined-text-field-label-text-size: var(--font-size-sm)}#searchHelpTrigger{--md-icon-button-icon-size: 18px;--md-icon-button-state-layer-height: var(--button-height);--md-icon-button-state-layer-width: var(--button-height);height:var(--button-height);width:var(--button-height)}.search-help-anchor{position:relative;display:inline-flex}#searchHelp{display:none;position:absolute;top:100%;left:0;margin-top:var(--spacing-xs);z-index:var(--z-search-help);border:1px solid var(--border-medium);border-radius:var(--radius-md);padding:10px 14px;box-shadow:0 4px 12px var(--shadow-md);font-size:var(--font-size-sm);line-height:1.5;background:#1e293b;color:#fff}#searchHelp.open{display:block}#searchHelp code{background:#ffffff1f;padding:1px var(--spacing-xs);border-radius:3px}#content{position:fixed;top:var(--toolbar-height);left:0;right:0;bottom:0}#graph{width:100%;height:100%;position:relative}.details-panel{position:absolute;top:var(--spacing-md);left:var(--spacing-md);width:fit-content;max-width:420px;height:auto;min-width:200px;min-height:120px;max-height:min(350px,calc(100% - var(--spacing-lg)));z-index:var(--z-details-panel);background:var(--bg-panel);box-shadow:0 2px 12px var(--shadow-sm);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);display:none;flex-direction:column;overflow:hidden;resize:both}.details-panel.open{display:flex}.details-panel .panel-header{display:flex;align-items:center;height:var(--button-height);padding:0 2px 0 10px;border-bottom:1px solid var(--border-subtle);flex-shrink:0;cursor:grab;-webkit-user-select:none;user-select:none}.details-panel .panel-header:active{cursor:grabbing}.details-panel .panel-title{font-size:var(--font-size-sm);font-weight:500;color:#333;flex:1}.details-panel md-icon-button{--md-icon-button-icon-size: 16px;--md-icon-button-state-layer-height: var(--icon-button-size);--md-icon-button-state-layer-width: var(--icon-button-size);height:var(--icon-button-size);width:var(--icon-button-size)}.details-panel .panel-body{flex:1;overflow-y:auto;min-height:0;padding:var(--spacing-md);font-family:var(--font-family)}#detailsPanel{border-left:3px solid var(--accent-info)}#detailsPanel .panel-header{background:#1a73e80f}#contextMenu{display:none;position:fixed;z-index:var(--z-context-menu);background:var(--bg-panel);border:1px solid var(--border-medium);border-radius:var(--spacing-sm);padding:var(--spacing-xs) 0;box-shadow:0 4px 12px var(--shadow-md);min-width:160px;font-size:var(--font-size-md)}.context-menu-item{padding:var(--spacing-sm) 14px;cursor:pointer;-webkit-user-select:none;user-select:none}.context-menu-item:hover{background:#0000000f}.context-menu-divider{height:1px;margin:var(--spacing-xs) 0;background:var(--border-medium)}#loading-overlay{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:var(--z-loading-overlay);background:#0000008c;color:#fff;font-family:var(--font-family);font-size:14px;padding:8px 18px;border-radius:var(--radius-md);pointer-events:none}#loading-overlay.visible{display:block}.tp-rotv{font-family:var(--font-family)!important;opacity:.9}#settingsPane{max-height:calc(100% - 30px);position:absolute;overflow-y:auto;right:10px;top:10px}#version-badge{position:fixed;bottom:var(--spacing-sm);right:var(--spacing-md);font-size:var(--font-size-xs);color:#0000004d;pointer-events:none;z-index:var(--z-version-badge);font-family:var(--font-family)}#zoom-indicator{position:fixed;bottom:var(--spacing-md);left:var(--spacing-md);z-index:var(--z-zoom-indicator);display:flex;align-items:center;gap:0;background:var(--bg-toolbar);border:1px solid var(--border-subtle);border-radius:var(--radius-md);box-shadow:0 1px 4px var(--shadow-xs);padding:0 var(--spacing-xs)}#zoom-indicator md-icon-button{--md-icon-button-icon-size: 16px;--md-icon-button-state-layer-height: var(--button-height);--md-icon-button-state-layer-width: var(--button-height);height:var(--button-height);width:var(--button-height)}#zoomLevel{font-size:var(--font-size-xs);font-family:var(--font-family);color:#555;min-width:34px;text-align:center;-webkit-user-select:none;user-select:none;cursor:pointer;border-radius:var(--radius-sm);padding:2px var(--spacing-xs);transition:background .1s}#zoomLevel:hover{background:#0000000f}.muted{color:#666}hr{border:none;border-top:1px solid var(--border-subtle);margin:var(--spacing-sm) 0}
|