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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h4-debug
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Advanced application proxy debugger and F12-style web console
5
5
  Author-email: h4 <h4@example.com>
6
6
  Requires-Python: >=3.8
@@ -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
- ws_url = f"ws://127.0.0.1:{self.port}/ws/telemetry"
31
- if not connect:
32
- return
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
- # Use original socket context if websockets internally creates sockets
37
- # This might be tricky because websockets uses asyncio or sync wrappers
38
- # which use the monkeypatched socket. We must be very careful.
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
- msg = self.queue.get()
44
- websocket.send(json.dumps(msg))
45
- self.queue.task_done()
46
- except Exception as e:
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 event == "call":
152
- func_name = frame.f_code.co_name
153
- filename = frame.f_code.co_filename
154
- # Ignore our own files and standard library to avoid noise, unless Full Debug
155
- if "h4_debug" not in filename and ("site-packages" not in filename or _mode == "Full"):
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": func_name,
218
+ "function": frame.f_code.co_name,
158
219
  "file": filename,
159
220
  "line": frame.f_lineno
160
221
  })
161
- elif event == "exception" and _mode in ("Trace", "Full"):
162
- exc_type, exc_value, exc_traceback = arg
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
- # Enable Tracing if required
193
- if mode in ("Trace", "Full"):
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", {"mode": mode, "pid": os.getpid()})
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">&gt;</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, "&amp;")
195
+ .replace(/</g, "&lt;")
196
+ .replace(/>/g, "&gt;")
197
+ .replace(/"/g, "&quot;")
198
+ .replace(/'/g, "&#039;");
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
+ });
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h4-debug
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Advanced application proxy debugger and F12-style web console
5
5
  Author-email: h4 <h4@example.com>
6
6
  Requires-Python: >=3.8
@@ -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.0"
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