Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit d625f20

Browse files
committed
feat(control-plane): reorganzied using yaml, parallel servers
1 parent cc7f84f commit d625f20

23 files changed

Lines changed: 258 additions & 1150 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,4 @@ keys/
225225

226226
# User configuration files
227227
config.json
228+
config.yaml
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414

1515
if __name__ in {"__main__", "__mp_main__"}:
1616
# Read port from config.json and start NiceGUI on config['port'] + 1 (default 8081).
17-
config = settings.load_config()
18-
edge_port = config.get('port', 8080)
19-
sidecar_port = edge_port + 1
17+
full_config = settings.load_config()
18+
cp_config = full_config.get('control_plane', {})
19+
mc_config = full_config.get('mobile_client', {})
20+
21+
edge_port = mc_config.get('port', 8080)
22+
sidecar_port = cp_config.get('port', edge_port + 1)
2023

2124
# Mount the dynamic video storage directory for browser playback
22-
upload_path = config.get('uploadPath', 'uploads/orgs/default')
25+
upload_path = mc_config.get('uploadPath', 'uploads/orgs/default')
2326
abs_upload_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', upload_path))
2427
os.makedirs(abs_upload_path, exist_ok=True)
2528
app.add_media_files('/videos', abs_upload_path)
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
from nicegui import ui
22
from theme import menu
3+
import subprocess
4+
import os
5+
import sys
6+
7+
def launch_rerun():
8+
ui.notify('Launching Rerun native viewer...', type='info')
9+
10+
# Try to launch the GUI/app.py from datacapture dataset
11+
gui_app = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..', 'allspark-datacapture', 'GUI', 'app.py'))
12+
dummy_server = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'dummy_rerun_server.py'))
13+
14+
if os.path.exists(gui_app):
15+
# We spawn the datacapture app
16+
subprocess.Popen([sys.executable, gui_app, '--root_folder', '/tmp', '--lean'])
17+
elif os.path.exists(dummy_server):
18+
# Or spawn a dummy server that serves on 9090
19+
subprocess.Popen([sys.executable, dummy_server])
20+
21+
ui.navigate.to('/rerun')
22+
323

424
def create_page():
525
@ui.page('/agent')
@@ -26,4 +46,4 @@ def agent_page():
2646
2747
I have generated a visualization of the synchronized data streams.
2848
''')
29-
ui.button('View in Rerun.io', icon='open_in_new', on_click=lambda: ui.navigate.to('/rerun')).classes('mt-2')
49+
ui.button('View in Rerun.io', icon='open_in_new', on_click=launch_rerun).classes('mt-2')

control_plane/pages/dashboard.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from nicegui import app, ui
2+
import json
3+
import time
4+
import os
5+
import glob
6+
import requests
7+
from theme import menu
8+
from pages.settings import load_config
9+
10+
anomalies = {}
11+
active_connections = []
12+
13+
def fetch_anomalies():
14+
full_config = load_config()
15+
cp_config = full_config.get('control_plane', {})
16+
anomaly_path = cp_config.get('logPaths', {}).get('anomalyLogs', 'logs/anomalies/')
17+
abs_anomaly_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', anomaly_path))
18+
19+
# Read JSON files in this directory
20+
new_anomalies = {}
21+
if os.path.exists(abs_anomaly_path):
22+
for file in glob.glob(os.path.join(abs_anomaly_path, '*.json')):
23+
try:
24+
with open(file, 'r') as f:
25+
data = json.load(f)
26+
27+
# Ensure source exists using filename or data
28+
source = data.get('source', 'unknown')
29+
if source == 'unknown':
30+
# Try to parse from filename: anomaly_rig1_timestamp.json
31+
basename = os.path.basename(file)
32+
if '_' in basename:
33+
source = basename.split('_')[1]
34+
35+
if source not in new_anomalies:
36+
new_anomalies[source] = []
37+
new_anomalies[source].append({
38+
"time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(file))),
39+
"data": data,
40+
"mtime": os.path.getmtime(file)
41+
})
42+
except Exception as e:
43+
pass
44+
45+
# Sort and keep latest 10
46+
global anomalies
47+
anomalies.clear()
48+
for source, events in new_anomalies.items():
49+
sorted_events = sorted(events, key=lambda x: x['mtime'])
50+
anomalies[source] = sorted_events[-10:]
51+
52+
53+
def fetch_connections():
54+
full_config = load_config()
55+
mc_config = full_config.get('mobile_client', {})
56+
edge_port = mc_config.get('port', 8080)
57+
try:
58+
resp = requests.get(f'http://127.0.0.1:{edge_port}/api/status', timeout=1)
59+
if resp.status_code == 200:
60+
data = resp.json()
61+
global active_connections
62+
active_connections = data.get('connections', [])
63+
except Exception:
64+
pass
65+
66+
67+
def create_page():
68+
@ui.page('/')
69+
def dashboard_page():
70+
val1 = load_config()
71+
cp_config = val1.get('control_plane', {})
72+
anomaly_path = cp_config.get('logPaths', {}).get('anomalyLogs', 'logs/anomalies/')
73+
74+
with menu('Dashboard'):
75+
# Fetch connections layout
76+
ui.label('Active Mobile Connections').classes('text-xl font-semibold mb-2 mt-4')
77+
conn_container = ui.list().classes('w-full border rounded p-2 mb-6')
78+
79+
def render_connections():
80+
fetch_connections()
81+
conn_container.clear()
82+
with conn_container:
83+
if not active_connections:
84+
ui.label('No active connections. Wait for mobile clients to pair...').classes('text-gray-500 italic p-4')
85+
return
86+
for conn in active_connections:
87+
ui.label(f'Active Device: {conn.get("clientName", "Unknown")} (ID: {conn.get("id")})').classes('font-bold')
88+
if conn.get("lastFilename"):
89+
ui.label(f' Last Upload: {conn["lastFilename"]} ({conn["lastFilesize"]} bytes)').classes('text-sm text-gray-600')
90+
91+
ui.timer(2.0, render_connections)
92+
93+
ui.label('System Anomalies').classes('text-xl font-semibold mb-2')
94+
container = ui.list().classes('w-full border rounded p-2')
95+
96+
def render_anomalies():
97+
fetch_anomalies()
98+
container.clear()
99+
with container:
100+
if not anomalies:
101+
ui.label(f'No anomalies detected yet. Watching: {anomaly_path}').classes('text-gray-500 italic p-4')
102+
return
103+
104+
for source, events in anomalies.items():
105+
title = f'Rig: {source} ({len(events)} events)'
106+
with ui.expansion(title, icon='warning').classes('w-full bg-gray-50 mb-2'):
107+
for ev in reversed(events):
108+
data_str = json.dumps(ev["data"])
109+
ui.label(f'Event: {ev["time"]} - {data_str}').classes('font-mono text-sm mb-1')
110+
ui.button('Investigate via Agent', on_click=lambda s=source: ui.navigate.to(f'/agent?source={s}')).classes('mt-2 bg-blue-500 text-white')
111+
112+
ui.timer(3.0, render_anomalies)
113+

0 commit comments

Comments
 (0)