h4-debug 0.1.0__tar.gz → 0.1.2__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.
- {h4_debug-0.1.0 → h4_debug-0.1.2}/PKG-INFO +1 -1
- h4_debug-0.1.2/h4_debug/__init__.py +1 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug/interceptor.py +118 -34
- h4_debug-0.1.2/h4_debug/server.py +137 -0
- h4_debug-0.1.2/h4_debug/static/dashboard.css +307 -0
- h4_debug-0.1.2/h4_debug/static/dashboard.html +94 -0
- h4_debug-0.1.2/h4_debug/static/dashboard.js +317 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/PKG-INFO +1 -1
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/SOURCES.txt +4 -1
- {h4_debug-0.1.0 → h4_debug-0.1.2}/pyproject.toml +4 -1
- h4_debug-0.1.0/h4_debug/__init__.py +0 -1
- h4_debug-0.1.0/h4_debug/server.py +0 -94
- {h4_debug-0.1.0 → h4_debug-0.1.2}/README.md +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug/cli.py +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/dependency_links.txt +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/entry_points.txt +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/requires.txt +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/h4_debug.egg-info/top_level.txt +0 -0
- {h4_debug-0.1.0 → h4_debug-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -7,13 +7,9 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
import traceback
|
|
10
|
+
import select
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
|
|
12
|
-
try:
|
|
13
|
-
from websockets.sync.client import connect
|
|
14
|
-
except ImportError:
|
|
15
|
-
connect = None
|
|
16
|
-
|
|
17
13
|
# We must keep references to original functions to avoid infinite recursion
|
|
18
14
|
_original_open = builtins.open
|
|
19
15
|
_original_socket = socket.socket
|
|
@@ -27,24 +23,80 @@ class TelemetryClient:
|
|
|
27
23
|
self.thread.start()
|
|
28
24
|
|
|
29
25
|
def _run(self):
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
# Connect to the local TCP server spawned by h4-debug
|
|
27
|
+
server_port = self.port + 1
|
|
28
|
+
|
|
34
29
|
while True:
|
|
35
30
|
try:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# A safer approach for the telemetry thread is to unpatch socket temporarily or
|
|
40
|
-
# ensure our hooks bypass if thread == telemetry_thread
|
|
41
|
-
with connect(ws_url) as websocket:
|
|
31
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conn:
|
|
32
|
+
conn.connect(("127.0.0.1", server_port))
|
|
33
|
+
|
|
42
34
|
while True:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
# 1. Send all queued outgoing telemetry
|
|
36
|
+
while not self.queue.empty():
|
|
37
|
+
try:
|
|
38
|
+
msg = self.queue.get_nowait()
|
|
39
|
+
conn.sendall((json.dumps(msg) + "\n").encode("utf-8"))
|
|
40
|
+
except queue.Empty:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
# 2. Check for incoming commands (with short timeout)
|
|
44
|
+
r, _, _ = select.select([conn], [], [], 0.05)
|
|
45
|
+
if r:
|
|
46
|
+
data = conn.recv(4096)
|
|
47
|
+
if not data:
|
|
48
|
+
break # Connection closed
|
|
49
|
+
for line in data.decode("utf-8").split("\n"):
|
|
50
|
+
if line.strip():
|
|
51
|
+
self._handle_command(line)
|
|
52
|
+
|
|
53
|
+
time.sleep(0.01)
|
|
54
|
+
except Exception:
|
|
47
55
|
time.sleep(1)
|
|
56
|
+
|
|
57
|
+
def _handle_command(self, cmd_data):
|
|
58
|
+
try:
|
|
59
|
+
cmd = json.loads(cmd_data)
|
|
60
|
+
action = cmd.get("action")
|
|
61
|
+
|
|
62
|
+
if action == "set_mode":
|
|
63
|
+
new_mode = cmd.get("mode", "Normal")
|
|
64
|
+
global _mode
|
|
65
|
+
_mode = new_mode
|
|
66
|
+
|
|
67
|
+
self.send("System", "info", {"text": f"Mode changed to {_mode}"})
|
|
68
|
+
|
|
69
|
+
elif action == "evaluate":
|
|
70
|
+
code = cmd.get("code", "")
|
|
71
|
+
result = None
|
|
72
|
+
is_error = False
|
|
73
|
+
|
|
74
|
+
# Try eval first, then exec
|
|
75
|
+
try:
|
|
76
|
+
result = eval(code, globals())
|
|
77
|
+
except SyntaxError:
|
|
78
|
+
try:
|
|
79
|
+
# Capture stdout for exec
|
|
80
|
+
import io
|
|
81
|
+
old_stdout = sys.stdout
|
|
82
|
+
sys.stdout = capture = io.StringIO()
|
|
83
|
+
exec(code, globals())
|
|
84
|
+
sys.stdout = old_stdout
|
|
85
|
+
result = capture.getvalue()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
is_error = True
|
|
88
|
+
result = traceback.format_exc()
|
|
89
|
+
except Exception as e:
|
|
90
|
+
is_error = True
|
|
91
|
+
result = traceback.format_exc()
|
|
92
|
+
|
|
93
|
+
self.send("Console", "eval_result", {
|
|
94
|
+
"code": code,
|
|
95
|
+
"result": str(result),
|
|
96
|
+
"is_error": is_error
|
|
97
|
+
})
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
48
100
|
|
|
49
101
|
def send(self, module, event_type, data):
|
|
50
102
|
msg = {
|
|
@@ -148,20 +200,27 @@ def trace_calls(frame, event, arg):
|
|
|
148
200
|
if _is_telemetry_thread() or not _telemetry:
|
|
149
201
|
return trace_calls
|
|
150
202
|
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
if _mode == "Normal":
|
|
204
|
+
return trace_calls
|
|
205
|
+
|
|
206
|
+
filename = frame.f_code.co_filename
|
|
207
|
+
if "h4_debug" in filename or "websockets" in filename:
|
|
208
|
+
return trace_calls # Ignore our own debugger code
|
|
209
|
+
|
|
210
|
+
is_user_code = "site-packages" not in filename and "lib" not in filename.lower()
|
|
211
|
+
|
|
212
|
+
if _mode == "Trace":
|
|
213
|
+
if not is_user_code:
|
|
214
|
+
return trace_calls
|
|
215
|
+
|
|
216
|
+
if event == "call":
|
|
156
217
|
_telemetry.send("Execution", "call", {
|
|
157
|
-
"function":
|
|
218
|
+
"function": frame.f_code.co_name,
|
|
158
219
|
"file": filename,
|
|
159
220
|
"line": frame.f_lineno
|
|
160
221
|
})
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
filename = frame.f_code.co_filename
|
|
164
|
-
if "h4_debug" not in filename:
|
|
222
|
+
elif event == "exception":
|
|
223
|
+
exc_type, exc_value, _ = arg
|
|
165
224
|
_telemetry.send("Execution", "exception", {
|
|
166
225
|
"type": exc_type.__name__,
|
|
167
226
|
"value": str(exc_value),
|
|
@@ -169,6 +228,28 @@ def trace_calls(frame, event, arg):
|
|
|
169
228
|
"line": frame.f_lineno
|
|
170
229
|
})
|
|
171
230
|
|
|
231
|
+
elif _mode == "Full":
|
|
232
|
+
if event == "line" and is_user_code:
|
|
233
|
+
_telemetry.send("Execution", "line", {
|
|
234
|
+
"function": frame.f_code.co_name,
|
|
235
|
+
"file": filename,
|
|
236
|
+
"line": frame.f_lineno
|
|
237
|
+
})
|
|
238
|
+
elif event in ("call", "return"):
|
|
239
|
+
_telemetry.send("Execution", event, {
|
|
240
|
+
"function": frame.f_code.co_name,
|
|
241
|
+
"file": filename,
|
|
242
|
+
"line": frame.f_lineno
|
|
243
|
+
})
|
|
244
|
+
elif event == "exception":
|
|
245
|
+
exc_type, exc_value, _ = arg
|
|
246
|
+
_telemetry.send("Execution", "exception", {
|
|
247
|
+
"type": exc_type.__name__,
|
|
248
|
+
"value": str(exc_value),
|
|
249
|
+
"file": filename,
|
|
250
|
+
"line": frame.f_lineno
|
|
251
|
+
})
|
|
252
|
+
|
|
172
253
|
return trace_calls
|
|
173
254
|
|
|
174
255
|
def start_interception(mode="Normal"):
|
|
@@ -189,9 +270,12 @@ def start_interception(mode="Normal"):
|
|
|
189
270
|
sys.stdout = PatchedStream(sys.stdout, "stdout")
|
|
190
271
|
sys.stderr = PatchedStream(sys.stderr, "stderr")
|
|
191
272
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
sys.settrace(trace_calls)
|
|
273
|
+
# ALWAYS enable Tracing so dynamic mode switching works later
|
|
274
|
+
sys.settrace(trace_calls)
|
|
195
275
|
|
|
196
|
-
_telemetry.send("System", "init", {
|
|
197
|
-
|
|
276
|
+
_telemetry.send("System", "init", {
|
|
277
|
+
"mode": mode,
|
|
278
|
+
"pid": os.getpid(),
|
|
279
|
+
"language": "python",
|
|
280
|
+
"version": sys.version
|
|
281
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import uvicorn
|
|
5
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
6
|
+
from fastapi.responses import HTMLResponse
|
|
7
|
+
from fastapi.staticfiles import StaticFiles
|
|
8
|
+
|
|
9
|
+
app = FastAPI()
|
|
10
|
+
|
|
11
|
+
# Store connected dashboard clients and the active telemetry process
|
|
12
|
+
dashboard_clients = []
|
|
13
|
+
active_telemetry_client = None # This will now be a TCP socket
|
|
14
|
+
|
|
15
|
+
# For broadcasting logs to WebSockets
|
|
16
|
+
def broadcast_log(log_item_str):
|
|
17
|
+
try:
|
|
18
|
+
log_item = json.loads(log_item_str)
|
|
19
|
+
log_history.append(log_item)
|
|
20
|
+
if len(log_history) > MAX_HISTORY:
|
|
21
|
+
log_history.pop(0)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
disconnected_clients = []
|
|
26
|
+
# Needs to be scheduled on the asyncio loop
|
|
27
|
+
async def _send(client):
|
|
28
|
+
try:
|
|
29
|
+
await client.send_text(log_item_str)
|
|
30
|
+
except Exception:
|
|
31
|
+
disconnected_clients.append(client)
|
|
32
|
+
|
|
33
|
+
for client in dashboard_clients.copy():
|
|
34
|
+
asyncio.run_coroutine_threadsafe(_send(client), loop)
|
|
35
|
+
|
|
36
|
+
for client in disconnected_clients:
|
|
37
|
+
if client in dashboard_clients:
|
|
38
|
+
dashboard_clients.remove(client)
|
|
39
|
+
|
|
40
|
+
# To persist logs for when a dashboard connects slightly after startup
|
|
41
|
+
log_history = []
|
|
42
|
+
MAX_HISTORY = 5000
|
|
43
|
+
|
|
44
|
+
# Get the path to static files
|
|
45
|
+
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
|
46
|
+
if not os.path.exists(STATIC_DIR):
|
|
47
|
+
os.makedirs(STATIC_DIR)
|
|
48
|
+
|
|
49
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
50
|
+
|
|
51
|
+
@app.get("/")
|
|
52
|
+
async def get_dashboard():
|
|
53
|
+
dashboard_path = os.path.join(STATIC_DIR, "dashboard.html")
|
|
54
|
+
if os.path.exists(dashboard_path):
|
|
55
|
+
with open(dashboard_path, "r", encoding="utf-8") as f:
|
|
56
|
+
return HTMLResponse(f.read())
|
|
57
|
+
return HTMLResponse("<h1>Dashboard not found</h1>")
|
|
58
|
+
|
|
59
|
+
@app.websocket("/ws/dashboard")
|
|
60
|
+
async def websocket_dashboard(websocket: WebSocket):
|
|
61
|
+
"""Endpoint for the web dashboard to receive logs and send commands."""
|
|
62
|
+
await websocket.accept()
|
|
63
|
+
|
|
64
|
+
# Send history on connect
|
|
65
|
+
for log_item in log_history:
|
|
66
|
+
try:
|
|
67
|
+
await websocket.send_text(json.dumps(log_item))
|
|
68
|
+
except Exception:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
dashboard_clients.append(websocket)
|
|
72
|
+
try:
|
|
73
|
+
while True:
|
|
74
|
+
# Receive commands from dashboard
|
|
75
|
+
data = await websocket.receive_text()
|
|
76
|
+
cmd = json.loads(data)
|
|
77
|
+
|
|
78
|
+
if cmd.get("action") == "clear":
|
|
79
|
+
log_history.clear()
|
|
80
|
+
elif cmd.get("action") in ["evaluate", "set_mode"]:
|
|
81
|
+
# Forward commands to the active telemetry process via TCP
|
|
82
|
+
if active_telemetry_client:
|
|
83
|
+
try:
|
|
84
|
+
active_telemetry_client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
|
|
85
|
+
except Exception:
|
|
86
|
+
active_telemetry_client = None
|
|
87
|
+
except WebSocketDisconnect:
|
|
88
|
+
if websocket in dashboard_clients:
|
|
89
|
+
dashboard_clients.remove(websocket)
|
|
90
|
+
|
|
91
|
+
import socket
|
|
92
|
+
import threading
|
|
93
|
+
|
|
94
|
+
def run_tcp_server(port: int):
|
|
95
|
+
global active_telemetry_client
|
|
96
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
97
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
98
|
+
server.bind(("127.0.0.1", port + 1))
|
|
99
|
+
server.listen(1)
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
conn, addr = server.accept()
|
|
103
|
+
active_telemetry_client = conn
|
|
104
|
+
|
|
105
|
+
def handle_client(c):
|
|
106
|
+
global active_telemetry_client
|
|
107
|
+
f = c.makefile("r", encoding="utf-8")
|
|
108
|
+
try:
|
|
109
|
+
while True:
|
|
110
|
+
line = f.readline()
|
|
111
|
+
if not line:
|
|
112
|
+
break
|
|
113
|
+
broadcast_log(line)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
finally:
|
|
117
|
+
if active_telemetry_client == c:
|
|
118
|
+
active_telemetry_client = None
|
|
119
|
+
c.close()
|
|
120
|
+
|
|
121
|
+
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
|
|
122
|
+
|
|
123
|
+
def run_server(port: int):
|
|
124
|
+
# Disable uvicorn access logs to keep terminal clean
|
|
125
|
+
import logging
|
|
126
|
+
log = logging.getLogger("uvicorn.access")
|
|
127
|
+
log.setLevel(logging.WARNING)
|
|
128
|
+
|
|
129
|
+
global loop
|
|
130
|
+
loop = asyncio.new_event_loop()
|
|
131
|
+
asyncio.set_event_loop(loop)
|
|
132
|
+
|
|
133
|
+
threading.Thread(target=run_tcp_server, args=(port,), daemon=True).start()
|
|
134
|
+
|
|
135
|
+
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning", loop="asyncio")
|
|
136
|
+
server = uvicorn.Server(config)
|
|
137
|
+
loop.run_until_complete(server.serve())
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-primary: #1e1e1e;
|
|
3
|
+
--bg-secondary: #252526;
|
|
4
|
+
--bg-hover: #2a2d2e;
|
|
5
|
+
--border-color: #3e3e42;
|
|
6
|
+
--text-primary: #d4d4d4;
|
|
7
|
+
--text-secondary: #cccccc;
|
|
8
|
+
--accent: #007acc;
|
|
9
|
+
--danger: #f14c4c;
|
|
10
|
+
--warning: #cca700;
|
|
11
|
+
--success: #89d185;
|
|
12
|
+
--font-family: 'Consolas', 'Courier New', monospace;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: light) {
|
|
16
|
+
body:not(.theme-dark) {
|
|
17
|
+
--bg-primary: #ffffff;
|
|
18
|
+
--bg-secondary: #f3f3f3;
|
|
19
|
+
--bg-hover: #e8e8e8;
|
|
20
|
+
--border-color: #cccccc;
|
|
21
|
+
--text-primary: #333333;
|
|
22
|
+
--text-secondary: #666666;
|
|
23
|
+
--accent: #0078d4;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body.theme-light {
|
|
28
|
+
--bg-primary: #ffffff;
|
|
29
|
+
--bg-secondary: #f3f3f3;
|
|
30
|
+
--bg-hover: #e8e8e8;
|
|
31
|
+
--border-color: #cccccc;
|
|
32
|
+
--text-primary: #333333;
|
|
33
|
+
--text-secondary: #666666;
|
|
34
|
+
--accent: #0078d4;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body.theme-dark {
|
|
38
|
+
--bg-primary: #1e1e1e;
|
|
39
|
+
--bg-secondary: #252526;
|
|
40
|
+
--bg-hover: #2a2d2e;
|
|
41
|
+
--border-color: #3e3e42;
|
|
42
|
+
--text-primary: #d4d4d4;
|
|
43
|
+
--text-secondary: #cccccc;
|
|
44
|
+
--accent: #007acc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
* {
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
body {
|
|
54
|
+
background-color: var(--bg-primary);
|
|
55
|
+
color: var(--text-primary);
|
|
56
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
57
|
+
height: 100vh;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.app-container {
|
|
62
|
+
display: flex;
|
|
63
|
+
height: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Sidebar */
|
|
67
|
+
.sidebar {
|
|
68
|
+
width: 250px;
|
|
69
|
+
background-color: var(--bg-secondary);
|
|
70
|
+
border-right: 1px solid var(--border-color);
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.logo {
|
|
76
|
+
padding: 20px;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
border-bottom: 1px solid var(--border-color);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.logo h1 {
|
|
84
|
+
font-size: 1.2rem;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.status-indicator {
|
|
89
|
+
width: 10px;
|
|
90
|
+
height: 10px;
|
|
91
|
+
border-radius: 50%;
|
|
92
|
+
background-color: var(--danger);
|
|
93
|
+
transition: background-color 0.3s ease;
|
|
94
|
+
}
|
|
95
|
+
.status-indicator.connected {
|
|
96
|
+
background-color: var(--success);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.nav-links {
|
|
100
|
+
list-style: none;
|
|
101
|
+
flex-grow: 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.nav-links li {
|
|
105
|
+
padding: 15px 20px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
transition: background 0.2s;
|
|
108
|
+
border-left: 3px solid transparent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.nav-links li:hover {
|
|
112
|
+
background-color: var(--bg-hover);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.nav-links li.active {
|
|
116
|
+
background-color: var(--bg-hover);
|
|
117
|
+
border-left-color: var(--accent);
|
|
118
|
+
font-weight: bold;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.controls {
|
|
122
|
+
padding: 20px;
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: 10px;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
button {
|
|
129
|
+
background-color: var(--bg-primary);
|
|
130
|
+
color: var(--text-primary);
|
|
131
|
+
border: 1px solid var(--border-color);
|
|
132
|
+
padding: 8px 12px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
transition: background 0.2s;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
button:hover {
|
|
139
|
+
background-color: var(--bg-hover);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Main Content */
|
|
143
|
+
.content-area {
|
|
144
|
+
flex-grow: 1;
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
min-width: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.toolbar {
|
|
151
|
+
padding: 10px 20px;
|
|
152
|
+
border-bottom: 1px solid var(--border-color);
|
|
153
|
+
display: flex;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
background-color: var(--bg-secondary);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toolbar input {
|
|
159
|
+
background-color: var(--bg-primary);
|
|
160
|
+
border: 1px solid var(--border-color);
|
|
161
|
+
color: var(--text-primary);
|
|
162
|
+
padding: 6px 12px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
width: 300px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.toggles label {
|
|
168
|
+
margin-left: 15px;
|
|
169
|
+
font-size: 0.9rem;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tab-content {
|
|
174
|
+
display: none;
|
|
175
|
+
flex-grow: 1;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.tab-content.active {
|
|
180
|
+
display: flex;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.log-container {
|
|
185
|
+
flex-grow: 1;
|
|
186
|
+
overflow-y: auto;
|
|
187
|
+
padding: 10px;
|
|
188
|
+
font-family: var(--font-family);
|
|
189
|
+
font-size: 0.9rem;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Log Entries */
|
|
193
|
+
.log-entry {
|
|
194
|
+
padding: 4px 8px;
|
|
195
|
+
border-bottom: 1px solid var(--border-color);
|
|
196
|
+
word-wrap: break-word;
|
|
197
|
+
display: flex;
|
|
198
|
+
gap: 10px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.log-entry:hover {
|
|
202
|
+
background-color: var(--bg-hover);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.log-time {
|
|
206
|
+
color: var(--text-secondary);
|
|
207
|
+
min-width: 90px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.log-module {
|
|
211
|
+
color: var(--accent);
|
|
212
|
+
min-width: 80px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.log-data {
|
|
216
|
+
flex-grow: 1;
|
|
217
|
+
white-space: pre-wrap;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Specific log types */
|
|
221
|
+
.log-entry.error .log-data { color: var(--danger); }
|
|
222
|
+
.log-entry.warn .log-data { color: var(--warning); }
|
|
223
|
+
.log-entry.system .log-data { color: var(--success); }
|
|
224
|
+
.log-entry.eval_result .log-data { color: var(--accent); font-weight: bold; }
|
|
225
|
+
.log-entry.eval_result.error .log-data { color: var(--danger); font-weight: normal; }
|
|
226
|
+
|
|
227
|
+
/* REPL Console Input */
|
|
228
|
+
.repl-container {
|
|
229
|
+
display: flex;
|
|
230
|
+
padding: 10px;
|
|
231
|
+
border-top: 1px solid var(--border-color);
|
|
232
|
+
background-color: var(--bg-secondary);
|
|
233
|
+
align-items: center;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.repl-prompt {
|
|
237
|
+
color: var(--accent);
|
|
238
|
+
font-weight: bold;
|
|
239
|
+
margin-right: 10px;
|
|
240
|
+
font-family: var(--font-family);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#repl-input {
|
|
244
|
+
flex-grow: 1;
|
|
245
|
+
background: transparent;
|
|
246
|
+
border: none;
|
|
247
|
+
color: var(--text-primary);
|
|
248
|
+
font-family: var(--font-family);
|
|
249
|
+
font-size: 0.9rem;
|
|
250
|
+
outline: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* Split View for Network */
|
|
254
|
+
.split-view {
|
|
255
|
+
display: flex;
|
|
256
|
+
height: 100%;
|
|
257
|
+
}
|
|
258
|
+
.list-pane {
|
|
259
|
+
width: 50%;
|
|
260
|
+
border-right: 1px solid var(--border-color);
|
|
261
|
+
overflow-y: auto;
|
|
262
|
+
}
|
|
263
|
+
.detail-pane {
|
|
264
|
+
width: 50%;
|
|
265
|
+
padding: 20px;
|
|
266
|
+
overflow-y: auto;
|
|
267
|
+
font-family: var(--font-family);
|
|
268
|
+
white-space: pre-wrap;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.network-item {
|
|
272
|
+
padding: 8px 10px;
|
|
273
|
+
border-bottom: 1px solid var(--border-color);
|
|
274
|
+
cursor: pointer;
|
|
275
|
+
font-family: var(--font-family);
|
|
276
|
+
font-size: 0.85rem;
|
|
277
|
+
}
|
|
278
|
+
.network-item:hover, .network-item.selected {
|
|
279
|
+
background-color: var(--bg-hover);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Context Menu */
|
|
283
|
+
.context-menu {
|
|
284
|
+
display: none;
|
|
285
|
+
position: absolute;
|
|
286
|
+
background-color: var(--bg-secondary);
|
|
287
|
+
border: 1px solid var(--border-color);
|
|
288
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
|
289
|
+
z-index: 1000;
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
min-width: 150px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.context-menu ul {
|
|
295
|
+
list-style: none;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.context-menu li {
|
|
299
|
+
padding: 8px 15px;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
font-size: 0.9rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.context-menu li:hover {
|
|
305
|
+
background-color: var(--accent);
|
|
306
|
+
color: white;
|
|
307
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>h4-debug | Advanced Telemetry</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/dashboard.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app-container">
|
|
11
|
+
<!-- Sidebar / Navigation -->
|
|
12
|
+
<nav class="sidebar">
|
|
13
|
+
<div class="logo">
|
|
14
|
+
<h1>h4-debug</h1>
|
|
15
|
+
<span class="status-indicator"></span>
|
|
16
|
+
</div>
|
|
17
|
+
<ul class="nav-links">
|
|
18
|
+
<li data-tab="console" class="active">Console</li>
|
|
19
|
+
<li data-tab="network">Network</li>
|
|
20
|
+
<li data-tab="disk">Disk / Source</li>
|
|
21
|
+
<li data-tab="execution">Execution</li>
|
|
22
|
+
</ul>
|
|
23
|
+
<div class="controls">
|
|
24
|
+
<button id="btn-clear">Clear Logs</button>
|
|
25
|
+
<button id="btn-export">Export JSON</button>
|
|
26
|
+
</div>
|
|
27
|
+
</nav>
|
|
28
|
+
|
|
29
|
+
<!-- Main Content Area -->
|
|
30
|
+
<main class="content-area">
|
|
31
|
+
|
|
32
|
+
<!-- Toolbar -->
|
|
33
|
+
<div class="toolbar">
|
|
34
|
+
<input type="text" id="filter-input" placeholder="Filter logs...">
|
|
35
|
+
<div class="toggles">
|
|
36
|
+
<label>Mode:
|
|
37
|
+
<select id="mode-select">
|
|
38
|
+
<option value="Normal">Normal</option>
|
|
39
|
+
<option value="Trace">Trace</option>
|
|
40
|
+
<option value="Full">Full Debug</option>
|
|
41
|
+
</select>
|
|
42
|
+
</label>
|
|
43
|
+
<label>Theme:
|
|
44
|
+
<select id="theme-select">
|
|
45
|
+
<option value="system">System</option>
|
|
46
|
+
<option value="dark">Dark</option>
|
|
47
|
+
<option value="light">Light</option>
|
|
48
|
+
</select>
|
|
49
|
+
</label>
|
|
50
|
+
<label><input type="checkbox" id="toggle-info" checked> Info</label>
|
|
51
|
+
<label><input type="checkbox" id="toggle-warn" checked> Warn</label>
|
|
52
|
+
<label><input type="checkbox" id="toggle-error" checked> Error</label>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Tab Contents -->
|
|
57
|
+
<div id="tab-console" class="tab-content active">
|
|
58
|
+
<div class="log-container" id="console-logs"></div>
|
|
59
|
+
<div class="repl-container">
|
|
60
|
+
<span class="repl-prompt">></span>
|
|
61
|
+
<input type="text" id="repl-input" placeholder="Evaluate expression...">
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div id="tab-network" class="tab-content">
|
|
66
|
+
<div class="split-view">
|
|
67
|
+
<div class="list-pane" id="network-logs"></div>
|
|
68
|
+
<div class="detail-pane" id="network-details">Select a request to view details</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div id="tab-disk" class="tab-content">
|
|
73
|
+
<div class="log-container" id="disk-logs"></div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div id="tab-execution" class="tab-content">
|
|
77
|
+
<div class="log-container" id="execution-logs"></div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
</main>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Custom Context Menu -->
|
|
84
|
+
<div id="context-menu" class="context-menu">
|
|
85
|
+
<ul>
|
|
86
|
+
<li id="menu-copy">Copy</li>
|
|
87
|
+
<li id="menu-copy-curl">Copy as cURL</li>
|
|
88
|
+
<li id="menu-copy-hex">Copy Hex Dump</li>
|
|
89
|
+
</ul>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<script src="/static/dashboard.js"></script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
// UI Elements
|
|
3
|
+
const tabs = document.querySelectorAll('.nav-links li');
|
|
4
|
+
const tabContents = document.querySelectorAll('.tab-content');
|
|
5
|
+
const statusIndicator = document.querySelector('.status-indicator');
|
|
6
|
+
const filterInput = document.getElementById('filter-input');
|
|
7
|
+
const modeSelect = document.getElementById('mode-select');
|
|
8
|
+
const themeSelect = document.getElementById('theme-select');
|
|
9
|
+
const replInput = document.getElementById('repl-input');
|
|
10
|
+
|
|
11
|
+
const containers = {
|
|
12
|
+
console: document.getElementById('console-logs'),
|
|
13
|
+
network: document.getElementById('network-logs'),
|
|
14
|
+
disk: document.getElementById('disk-logs'),
|
|
15
|
+
execution: document.getElementById('execution-logs')
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const networkDetails = document.getElementById('network-details');
|
|
19
|
+
|
|
20
|
+
let allLogs = [];
|
|
21
|
+
let ws = null;
|
|
22
|
+
let selectedNetworkLog = null;
|
|
23
|
+
|
|
24
|
+
// Tabs logic
|
|
25
|
+
tabs.forEach(tab => {
|
|
26
|
+
tab.addEventListener('click', () => {
|
|
27
|
+
tabs.forEach(t => t.classList.remove('active'));
|
|
28
|
+
tabContents.forEach(c => c.classList.remove('active'));
|
|
29
|
+
|
|
30
|
+
tab.classList.add('active');
|
|
31
|
+
const targetId = `tab-${tab.dataset.tab}`;
|
|
32
|
+
document.getElementById(targetId).classList.add('active');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// WebSocket Connection
|
|
37
|
+
function connectWS() {
|
|
38
|
+
ws = new WebSocket(`ws://${window.location.host}/ws/dashboard`);
|
|
39
|
+
|
|
40
|
+
ws.onopen = () => {
|
|
41
|
+
statusIndicator.classList.add('connected');
|
|
42
|
+
appendLog({
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
module: 'System',
|
|
45
|
+
type: 'info',
|
|
46
|
+
data: { text: 'Dashboard connected to proxy backend.' }
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
ws.onclose = () => {
|
|
51
|
+
statusIndicator.classList.remove('connected');
|
|
52
|
+
setTimeout(connectWS, 2000); // Reconnect
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
ws.onmessage = (event) => {
|
|
56
|
+
try {
|
|
57
|
+
const log = JSON.parse(event.data);
|
|
58
|
+
allLogs.push(log);
|
|
59
|
+
processLog(log);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("Failed to parse log", e);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectWS();
|
|
67
|
+
|
|
68
|
+
// Log Processing
|
|
69
|
+
function processLog(log) {
|
|
70
|
+
if (!passesFilter(log, filterInput.value)) return;
|
|
71
|
+
|
|
72
|
+
const timeStr = new Date(log.timestamp).toLocaleTimeString([], {hour12: false, fractionalSecondDigits: 3});
|
|
73
|
+
|
|
74
|
+
if (log.module === 'Network') {
|
|
75
|
+
renderNetworkLog(log, timeStr);
|
|
76
|
+
} else if (log.module === 'Disk') {
|
|
77
|
+
renderBasicLog(containers.disk, log, timeStr);
|
|
78
|
+
} else if (log.module === 'Execution') {
|
|
79
|
+
renderBasicLog(containers.execution, log, timeStr);
|
|
80
|
+
} else if (log.module === 'Console') {
|
|
81
|
+
if (log.type === 'eval_result') {
|
|
82
|
+
renderEvalResult(containers.console, log, timeStr);
|
|
83
|
+
} else {
|
|
84
|
+
renderBasicLog(containers.console, log, timeStr);
|
|
85
|
+
}
|
|
86
|
+
} else if (log.module === 'System') {
|
|
87
|
+
if (log.type === 'init' && log.data.language) {
|
|
88
|
+
replInput.placeholder = `Evaluate ${log.data.language} expression...`;
|
|
89
|
+
if (log.data.mode) modeSelect.value = log.data.mode;
|
|
90
|
+
}
|
|
91
|
+
renderBasicLog(containers.console, log, timeStr, 'system');
|
|
92
|
+
} else {
|
|
93
|
+
renderBasicLog(containers.console, log, timeStr);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderBasicLog(container, log, timeStr, customClass='') {
|
|
98
|
+
const el = document.createElement('div');
|
|
99
|
+
el.className = `log-entry ${customClass}`;
|
|
100
|
+
el.dataset.raw = JSON.stringify(log);
|
|
101
|
+
|
|
102
|
+
const timeEl = document.createElement('div');
|
|
103
|
+
timeEl.className = 'log-time';
|
|
104
|
+
timeEl.textContent = timeStr;
|
|
105
|
+
|
|
106
|
+
const modEl = document.createElement('div');
|
|
107
|
+
modEl.className = 'log-module';
|
|
108
|
+
modEl.textContent = `[${log.module}]`;
|
|
109
|
+
|
|
110
|
+
const dataEl = document.createElement('div');
|
|
111
|
+
dataEl.className = 'log-data';
|
|
112
|
+
|
|
113
|
+
if (log.data && log.data.text) {
|
|
114
|
+
dataEl.textContent = log.data.text;
|
|
115
|
+
if (log.type === 'stderr' || log.type === 'exception') el.classList.add('error');
|
|
116
|
+
} else {
|
|
117
|
+
dataEl.textContent = JSON.stringify(log.data);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
el.appendChild(timeEl);
|
|
121
|
+
el.appendChild(modEl);
|
|
122
|
+
el.appendChild(dataEl);
|
|
123
|
+
|
|
124
|
+
container.appendChild(el);
|
|
125
|
+
scrollToBottom(container);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderEvalResult(container, log, timeStr) {
|
|
129
|
+
const el = document.createElement('div');
|
|
130
|
+
el.className = 'log-entry eval_result' + (log.data.is_error ? ' error' : '');
|
|
131
|
+
el.dataset.raw = JSON.stringify(log);
|
|
132
|
+
|
|
133
|
+
const timeEl = document.createElement('div');
|
|
134
|
+
timeEl.className = 'log-time';
|
|
135
|
+
timeEl.textContent = timeStr;
|
|
136
|
+
|
|
137
|
+
const modEl = document.createElement('div');
|
|
138
|
+
modEl.className = 'log-module';
|
|
139
|
+
modEl.textContent = `[Eval]`;
|
|
140
|
+
|
|
141
|
+
const dataEl = document.createElement('div');
|
|
142
|
+
dataEl.className = 'log-data';
|
|
143
|
+
dataEl.textContent = log.data.result;
|
|
144
|
+
|
|
145
|
+
el.appendChild(timeEl);
|
|
146
|
+
el.appendChild(modEl);
|
|
147
|
+
el.appendChild(dataEl);
|
|
148
|
+
|
|
149
|
+
container.appendChild(el);
|
|
150
|
+
scrollToBottom(container);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderNetworkLog(log, timeStr) {
|
|
154
|
+
const el = document.createElement('div');
|
|
155
|
+
el.className = 'network-item';
|
|
156
|
+
el.dataset.raw = JSON.stringify(log);
|
|
157
|
+
|
|
158
|
+
let preview = '';
|
|
159
|
+
if (log.type === 'connect') preview = `CONNECT ${log.data.address}`;
|
|
160
|
+
else if (log.type === 'send') preview = `SEND ${log.data.bytes} bytes to ${log.data.address}`;
|
|
161
|
+
else if (log.type === 'recv') preview = `RECV ${log.data.bytes} bytes from ${log.data.address}`;
|
|
162
|
+
|
|
163
|
+
el.textContent = `${timeStr} - ${preview}`;
|
|
164
|
+
|
|
165
|
+
el.addEventListener('click', () => {
|
|
166
|
+
document.querySelectorAll('.network-item').forEach(i => i.classList.remove('selected'));
|
|
167
|
+
el.classList.add('selected');
|
|
168
|
+
selectedNetworkLog = log;
|
|
169
|
+
renderNetworkDetails(log);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
containers.network.appendChild(el);
|
|
173
|
+
scrollToBottom(containers.network);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderNetworkDetails(log) {
|
|
177
|
+
let html = `<h3>Network Event: ${log.type}</h3>`;
|
|
178
|
+
html += `<div><strong>Time:</strong> ${log.timestamp}</div>`;
|
|
179
|
+
if (log.data.address) html += `<div><strong>Address:</strong> ${log.data.address}</div>`;
|
|
180
|
+
if (log.data.bytes) html += `<div><strong>Bytes:</strong> ${log.data.bytes}</div>`;
|
|
181
|
+
|
|
182
|
+
if (log.data.payload_text) {
|
|
183
|
+
html += `<h4>Payload (Text)</h4><pre>${escapeHtml(log.data.payload_text)}</pre>`;
|
|
184
|
+
}
|
|
185
|
+
if (log.data.payload_hex) {
|
|
186
|
+
html += `<h4>Payload (Hex)</h4><pre>${log.data.payload_hex}</pre>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
networkDetails.innerHTML = html;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function escapeHtml(unsafe) {
|
|
193
|
+
return (unsafe || '').toString()
|
|
194
|
+
.replace(/&/g, "&")
|
|
195
|
+
.replace(/</g, "<")
|
|
196
|
+
.replace(/>/g, ">")
|
|
197
|
+
.replace(/"/g, """)
|
|
198
|
+
.replace(/'/g, "'");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function scrollToBottom(el) {
|
|
202
|
+
if (el.scrollHeight - el.scrollTop < el.clientHeight + 100) {
|
|
203
|
+
el.scrollTop = el.scrollHeight;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function passesFilter(log, term) {
|
|
208
|
+
if (!term) return true;
|
|
209
|
+
return JSON.stringify(log).toLowerCase().includes(term.toLowerCase());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Filter Logic
|
|
213
|
+
filterInput.addEventListener('input', () => {
|
|
214
|
+
Object.values(containers).forEach(c => c.innerHTML = '');
|
|
215
|
+
allLogs.forEach(processLog);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Mode Toggle Logic
|
|
219
|
+
modeSelect.addEventListener('change', (e) => {
|
|
220
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
221
|
+
ws.send(JSON.stringify({
|
|
222
|
+
action: 'set_mode',
|
|
223
|
+
mode: e.target.value
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Theme Toggle Logic
|
|
229
|
+
themeSelect.addEventListener('change', (e) => {
|
|
230
|
+
const theme = e.target.value;
|
|
231
|
+
if (theme === 'dark') {
|
|
232
|
+
document.body.classList.add('theme-dark');
|
|
233
|
+
document.body.classList.remove('theme-light');
|
|
234
|
+
} else if (theme === 'light') {
|
|
235
|
+
document.body.classList.add('theme-light');
|
|
236
|
+
document.body.classList.remove('theme-dark');
|
|
237
|
+
} else {
|
|
238
|
+
document.body.classList.remove('theme-light', 'theme-dark');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// REPL Console Logic
|
|
243
|
+
replInput.addEventListener('keydown', (e) => {
|
|
244
|
+
if (e.key === 'Enter' && replInput.value.trim() !== '') {
|
|
245
|
+
const code = replInput.value;
|
|
246
|
+
// Echo locally
|
|
247
|
+
renderBasicLog(containers.console, {
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
module: 'Console',
|
|
250
|
+
type: 'input',
|
|
251
|
+
data: { text: `> ${code}` }
|
|
252
|
+
}, new Date().toLocaleTimeString([], {hour12: false, fractionalSecondDigits: 3}));
|
|
253
|
+
|
|
254
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
255
|
+
ws.send(JSON.stringify({
|
|
256
|
+
action: 'evaluate',
|
|
257
|
+
code: code
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
replInput.value = '';
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Clear Button
|
|
265
|
+
document.getElementById('btn-clear').addEventListener('click', () => {
|
|
266
|
+
allLogs = [];
|
|
267
|
+
Object.values(containers).forEach(c => c.innerHTML = '');
|
|
268
|
+
networkDetails.innerHTML = 'Select a request to view details';
|
|
269
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
270
|
+
ws.send(JSON.stringify({action: 'clear'}));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Export Button
|
|
275
|
+
document.getElementById('btn-export').addEventListener('click', () => {
|
|
276
|
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(allLogs, null, 2));
|
|
277
|
+
const anchor = document.createElement('a');
|
|
278
|
+
anchor.setAttribute("href", dataStr);
|
|
279
|
+
anchor.setAttribute("download", "h4_debug_export.json");
|
|
280
|
+
document.body.appendChild(anchor);
|
|
281
|
+
anchor.click();
|
|
282
|
+
anchor.remove();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Context Menu Logic
|
|
286
|
+
const contextMenu = document.getElementById('context-menu');
|
|
287
|
+
let contextTargetData = null;
|
|
288
|
+
|
|
289
|
+
document.addEventListener('contextmenu', (e) => {
|
|
290
|
+
const entry = e.target.closest('.log-entry') || e.target.closest('.network-item');
|
|
291
|
+
if (entry) {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
contextTargetData = JSON.parse(entry.dataset.raw);
|
|
294
|
+
contextMenu.style.display = 'block';
|
|
295
|
+
contextMenu.style.left = `${e.pageX}px`;
|
|
296
|
+
contextMenu.style.top = `${e.pageY}px`;
|
|
297
|
+
} else {
|
|
298
|
+
contextMenu.style.display = 'none';
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
document.addEventListener('click', () => {
|
|
303
|
+
contextMenu.style.display = 'none';
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
document.getElementById('menu-copy').addEventListener('click', () => {
|
|
307
|
+
if (contextTargetData) {
|
|
308
|
+
navigator.clipboard.writeText(JSON.stringify(contextTargetData, null, 2));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
document.getElementById('menu-copy-hex').addEventListener('click', () => {
|
|
313
|
+
if (contextTargetData && contextTargetData.data.payload_hex) {
|
|
314
|
+
navigator.clipboard.writeText(contextTargetData.data.payload_hex);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -9,4 +9,7 @@ h4_debug.egg-info/SOURCES.txt
|
|
|
9
9
|
h4_debug.egg-info/dependency_links.txt
|
|
10
10
|
h4_debug.egg-info/entry_points.txt
|
|
11
11
|
h4_debug.egg-info/requires.txt
|
|
12
|
-
h4_debug.egg-info/top_level.txt
|
|
12
|
+
h4_debug.egg-info/top_level.txt
|
|
13
|
+
h4_debug/static/dashboard.css
|
|
14
|
+
h4_debug/static/dashboard.html
|
|
15
|
+
h4_debug/static/dashboard.js
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "h4-debug"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Advanced application proxy debugger and F12-style web console"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -19,3 +19,6 @@ dependencies = [
|
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
h4-debug = "h4_debug.cli:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
h4_debug = ["static/*"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import uvicorn
|
|
5
|
-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
6
|
-
from fastapi.responses import HTMLResponse
|
|
7
|
-
from fastapi.staticfiles import StaticFiles
|
|
8
|
-
|
|
9
|
-
app = FastAPI()
|
|
10
|
-
|
|
11
|
-
# Store connected dashboard clients
|
|
12
|
-
dashboard_clients = []
|
|
13
|
-
|
|
14
|
-
# To persist logs for when a dashboard connects slightly after startup
|
|
15
|
-
log_history = []
|
|
16
|
-
MAX_HISTORY = 5000
|
|
17
|
-
|
|
18
|
-
# Get the path to static files
|
|
19
|
-
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
|
20
|
-
if not os.path.exists(STATIC_DIR):
|
|
21
|
-
os.makedirs(STATIC_DIR)
|
|
22
|
-
|
|
23
|
-
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
24
|
-
|
|
25
|
-
@app.get("/")
|
|
26
|
-
async def get_dashboard():
|
|
27
|
-
dashboard_path = os.path.join(STATIC_DIR, "dashboard.html")
|
|
28
|
-
if os.path.exists(dashboard_path):
|
|
29
|
-
with open(dashboard_path, "r", encoding="utf-8") as f:
|
|
30
|
-
return HTMLResponse(f.read())
|
|
31
|
-
return HTMLResponse("<h1>Dashboard not found</h1>")
|
|
32
|
-
|
|
33
|
-
@app.websocket("/ws/telemetry")
|
|
34
|
-
async def websocket_telemetry(websocket: WebSocket):
|
|
35
|
-
"""Endpoint for the intercepted process to push logs."""
|
|
36
|
-
await websocket.accept()
|
|
37
|
-
try:
|
|
38
|
-
while True:
|
|
39
|
-
data = await websocket.receive_text()
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
log_item = json.loads(data)
|
|
43
|
-
log_history.append(log_item)
|
|
44
|
-
if len(log_history) > MAX_HISTORY:
|
|
45
|
-
log_history.pop(0)
|
|
46
|
-
except Exception:
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
# Broadcast to all dashboards
|
|
50
|
-
disconnected_clients = []
|
|
51
|
-
for client in dashboard_clients:
|
|
52
|
-
try:
|
|
53
|
-
await client.send_text(data)
|
|
54
|
-
except Exception:
|
|
55
|
-
disconnected_clients.append(client)
|
|
56
|
-
|
|
57
|
-
for client in disconnected_clients:
|
|
58
|
-
if client in dashboard_clients:
|
|
59
|
-
dashboard_clients.remove(client)
|
|
60
|
-
except WebSocketDisconnect:
|
|
61
|
-
pass
|
|
62
|
-
|
|
63
|
-
@app.websocket("/ws/dashboard")
|
|
64
|
-
async def websocket_dashboard(websocket: WebSocket):
|
|
65
|
-
"""Endpoint for the web dashboard to receive logs."""
|
|
66
|
-
await websocket.accept()
|
|
67
|
-
|
|
68
|
-
# Send history on connect
|
|
69
|
-
for log_item in log_history:
|
|
70
|
-
try:
|
|
71
|
-
await websocket.send_text(json.dumps(log_item))
|
|
72
|
-
except Exception:
|
|
73
|
-
break
|
|
74
|
-
|
|
75
|
-
dashboard_clients.append(websocket)
|
|
76
|
-
try:
|
|
77
|
-
while True:
|
|
78
|
-
# Keep alive and receive commands from dashboard if needed
|
|
79
|
-
data = await websocket.receive_text()
|
|
80
|
-
cmd = json.loads(data)
|
|
81
|
-
if cmd.get("action") == "clear":
|
|
82
|
-
log_history.clear()
|
|
83
|
-
except WebSocketDisconnect:
|
|
84
|
-
if websocket in dashboard_clients:
|
|
85
|
-
dashboard_clients.remove(websocket)
|
|
86
|
-
|
|
87
|
-
def run_server(port: int):
|
|
88
|
-
# Disable uvicorn access logs to keep terminal clean
|
|
89
|
-
import logging
|
|
90
|
-
log = logging.getLogger("uvicorn.access")
|
|
91
|
-
log.setLevel(logging.WARNING)
|
|
92
|
-
|
|
93
|
-
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
|
|
94
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|