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.
Files changed (33) hide show
  1. {sysgraph-0.0.17/src/sysgraph.egg-info → sysgraph-0.0.18}/PKG-INFO +17 -14
  2. {sysgraph-0.0.17 → sysgraph-0.0.18}/README.md +13 -12
  3. {sysgraph-0.0.17 → sysgraph-0.0.18}/pyproject.toml +3 -1
  4. sysgraph-0.0.18/src/sysgraph/__init__.py +1 -0
  5. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/app.py +6 -1
  6. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/constants.py +2 -2
  7. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/discovery.py +41 -12
  8. sysgraph-0.0.18/src/sysgraph/dist/assets/index-BLbtXZbQ.css +1 -0
  9. sysgraph-0.0.17/src/sysgraph/dist/assets/index-BlTIaOJH.js → sysgraph-0.0.18/src/sysgraph/dist/assets/index-Dq5MCDEE.js +98 -98
  10. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/dist/index.html +6 -4
  11. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/main.py +4 -1
  12. {sysgraph-0.0.17 → sysgraph-0.0.18/src/sysgraph.egg-info}/PKG-INFO +17 -14
  13. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/SOURCES.txt +2 -2
  14. sysgraph-0.0.17/src/sysgraph/__init__.py +0 -1
  15. sysgraph-0.0.17/src/sysgraph/dist/assets/index-BBuhcA5M.css +0 -1
  16. {sysgraph-0.0.17 → sysgraph-0.0.18}/LICENSE +0 -0
  17. {sysgraph-0.0.17 → sysgraph-0.0.18}/MANIFEST.in +0 -0
  18. {sysgraph-0.0.17 → sysgraph-0.0.18}/setup.cfg +0 -0
  19. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/__main__.py +0 -0
  20. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/dist/assets/icon-TKtfQOgj.png +0 -0
  21. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/graph.py +0 -0
  22. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/model.py +0 -0
  23. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/compute_vc.py +0 -0
  24. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/convert_to_graph_format.py +0 -0
  25. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/histogram_weights.py +0 -0
  26. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/congress_network/viral_centrality.py +0 -0
  27. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/temp/voles_network/convert_voles_to_graph_format.py +0 -0
  28. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/tests/test_discovery.py +0 -0
  29. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph/tests/test_graph.py +0 -0
  30. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/dependency_links.txt +0 -0
  31. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/entry_points.txt +0 -0
  32. {sysgraph-0.0.17 → sysgraph-0.0.18}/src/sysgraph.egg-info/requires.txt +0 -0
  33. {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.17
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,procfs
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, no Linux or special privileges required
35
- - **Live process graph** — discover running OS processes and their inter-process communication channels (pipes, Unix domain sockets, TCP/UDP connections) in real time (Linux only)
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
+ [![PyPI](https://img.shields.io/pypi/v/sysgraph)](https://pypi.org/project/sysgraph/)
37
40
  ![Python](https://img.shields.io/badge/python-%3E%3D3.12-blue)
38
41
  ![License](https://img.shields.io/badge/license-MIT-green)
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** *(Linux only)* — enumerates running OS processes and their parent-child relationships
49
- - **IPC visualization** *(Linux only)* — discovers pipes, Unix domain sockets, and TCP/UDP connections between processes
50
- - **Real-time** *(Linux only)* — fetch the latest process graph on demand via the web UI
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** is required only for live process-graph discovery (relies on `/proc` and `ss`); importing and visualizing your own graphs works on any platform
60
- - Root/sudo recommended for full process visibility (Linux only)
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 (Linux only)
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`) to discover processes, pipes, Unix domain sockets, and network connections, building a graph served via `GET /api/graph`.
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, Linux, Docker
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/ # requires Linux /proc
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, no Linux or special privileges required
6
- - **Live process graph** — discover running OS processes and their inter-process communication channels (pipes, Unix domain sockets, TCP/UDP connections) in real time (Linux only)
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
+ [![PyPI](https://img.shields.io/pypi/v/sysgraph)](https://pypi.org/project/sysgraph/)
8
9
  ![Python](https://img.shields.io/badge/python-%3E%3D3.12-blue)
9
10
  ![License](https://img.shields.io/badge/license-MIT-green)
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** *(Linux only)* — enumerates running OS processes and their parent-child relationships
20
- - **IPC visualization** *(Linux only)* — discovers pipes, Unix domain sockets, and TCP/UDP connections between processes
21
- - **Real-time** *(Linux only)* — fetch the latest process graph on demand via the web UI
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** is required only for live process-graph discovery (relies on `/proc` and `ss`); importing and visualizing your own graphs works on any platform
31
- - Root/sudo recommended for full process visibility (Linux only)
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 (Linux only)
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`) to discover processes, pipes, Unix domain sockets, and network connections, building a graph served via `GET /api/graph`.
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, Linux, Docker
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/ # requires Linux /proc
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", "linux", "procfs"]
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(inode: int) -> str:
24
- return f"{NODE_UDS}::{inode}"
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.shared
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 as reported by kernel.
86
+ """Discover Unix Domain Sockets by parsing ``ss -xp`` output.
85
87
 
86
- Note however that connection with UDS will have at least 2 records reported
87
- with opposite local/peer inodes. They can be further groupped to
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]: _description_
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(s: str) -> list[UnixDomainSocketProcRef]:
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]+),fd=(?P<fd>[0-9]+)\)', s
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 inodes.
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 (uds.local_inode, uds.peer_inode) not in visited and (
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 directly from /proc instead of spawning lsof."""
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}