forked from enderofwings/NexusOS
feat: sync with upstream — v1.2.0, in-app updates, Projects, modules
Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
+15
-89
@@ -1,4 +1,3 @@
|
||||
# /home/jon/nexus-core/management/controlpanel.py
|
||||
|
||||
import time
|
||||
import tkinter as tk
|
||||
@@ -20,7 +19,9 @@ try:
|
||||
except Exception:
|
||||
VERSION = "0.0.0"
|
||||
|
||||
# Force 0.0.0.0 to ensure the Windows Host bridge works for Project Nexus
|
||||
# Loopback by default -- the REST APIs are unauthenticated, so 0.0.0.0 exposed
|
||||
# the whole admin+data plane to the LAN. Same knob management/ncp.py uses.
|
||||
BIND_HOST = os.environ.get("NEXUS_BIND_HOST", "127.0.0.1")
|
||||
FRONTEND_DIR = PROJECT_ROOT / "interface" / "web"
|
||||
def _find_npm():
|
||||
"""Prefer nvm's newest node. When the panel is launched from the desktop
|
||||
@@ -51,28 +52,15 @@ BACKEND_CMD = [
|
||||
str(PROJECT_ROOT / "Promethean" / "bin" / "python"),
|
||||
"-m", "uvicorn",
|
||||
"synapse.main:sio_app",
|
||||
"--host", "0.0.0.0",
|
||||
"--host", BIND_HOST,
|
||||
"--port", "8000",
|
||||
"--reload",
|
||||
"--reload-dir", str(PROJECT_ROOT / "synapse"),
|
||||
"--log-level", "info"
|
||||
]
|
||||
|
||||
MEMORY_DIR = PROJECT_ROOT
|
||||
MEMORY_CMD = [
|
||||
str(PROJECT_ROOT / "Promethean" / "bin" / "python"),
|
||||
"-m", "uvicorn",
|
||||
"synapse.memory.service:app",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "8001",
|
||||
"--reload",
|
||||
"--reload-dir", str(PROJECT_ROOT / "synapse"),
|
||||
"--log-level", "info"
|
||||
]
|
||||
|
||||
FRONTEND_PID = PROJECT_ROOT / "runtime" / "pids" / "frontend.pid"
|
||||
BACKEND_PID = PROJECT_ROOT / "runtime" / "pids" / "backend.pid"
|
||||
MEMORY_PID = PROJECT_ROOT / "runtime" / "pids" / "memory.pid"
|
||||
CHAT_LOG = PROJECT_ROOT / "runtime" / "logs" / "chat.log"
|
||||
|
||||
class NexusControlPanel:
|
||||
@@ -109,10 +97,8 @@ class NexusControlPanel:
|
||||
|
||||
self.frontend_process = None
|
||||
self.backend_process = None
|
||||
self.memory_process = None
|
||||
self._frontend_starting = False
|
||||
self._backend_starting = False
|
||||
self._memory_starting = False
|
||||
self._closing = False
|
||||
|
||||
self._setup_custom_titlebar()
|
||||
@@ -132,9 +118,6 @@ class NexusControlPanel:
|
||||
if self._is_running(frontend_pid):
|
||||
threading.Thread(target=self._monitor_frontend, daemon=True).start()
|
||||
|
||||
if self._is_running(MEMORY_PID):
|
||||
threading.Thread(target=self._monitor_memory, daemon=True).start()
|
||||
|
||||
threading.Thread(target=self._monitor_synapses, daemon=True).start()
|
||||
|
||||
self.update_master_ui()
|
||||
@@ -167,12 +150,11 @@ class NexusControlPanel:
|
||||
|
||||
|
||||
def _init_log_tags(self):
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console, self.mem_console]:
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console]:
|
||||
console.tag_config("SYSTEM", foreground="#ff00ea")
|
||||
console.tag_config("FRONTEND", foreground="#61dbfb")
|
||||
console.tag_config("BACKEND", foreground="#ffd43b")
|
||||
console.tag_config("MINDTRACE", foreground="#cc88ff")
|
||||
console.tag_config("MEMORY", foreground="#78e08f")
|
||||
console.tag_config("ERROR", foreground="#ff0000")
|
||||
console.tag_config("GPU_NVIDIA", foreground="#76b900")
|
||||
console.tag_config("GPU_AMD", foreground="#ed1c24")
|
||||
@@ -193,7 +175,7 @@ class NexusControlPanel:
|
||||
self.root.geometry(f"+{self.root.winfo_x() + (event.x - self.x)}+{self.root.winfo_y() + (event.y - self.y)}")
|
||||
|
||||
def _clear_all_logs(self):
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mem_console, self.mind_console]:
|
||||
for console in [self.sys_console, self.front_console, self.back_console, self.mind_console]:
|
||||
console.delete("1.0", tk.END)
|
||||
|
||||
def _copy_selection(self, widget):
|
||||
@@ -260,19 +242,13 @@ class NexusControlPanel:
|
||||
self.btn_back_stop = tk.Button(controls, text="OFF", **STOP_STYLE, command=self.stop_backend)
|
||||
self.btn_back_stop.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Memory Service Controls
|
||||
self.btn_mem_start = tk.Button(controls, text="MEMORY", fg="#78e08f", **START_STYLE, command=self.start_memory)
|
||||
self.btn_mem_start.pack(side=tk.LEFT, padx=(8, 2))
|
||||
self.btn_mem_stop = tk.Button(controls, text="OFF", **STOP_STYLE, command=self.stop_memory)
|
||||
self.btn_mem_stop.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Terminal Displays
|
||||
sys_f, self.sys_console = self._create_terminal(self.root, " >> SYSTEM STATUS", height=5, text_color="#ff00ea")
|
||||
sys_f.pack(fill=tk.X, padx=10, pady=(0, 8))
|
||||
|
||||
bottom_f = tk.Frame(self.root, bg="#1e1e1e")
|
||||
bottom_f.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 8))
|
||||
for col in range(4):
|
||||
for col in range(3):
|
||||
bottom_f.columnconfigure(col, weight=1, uniform="term")
|
||||
bottom_f.rowconfigure(0, weight=1)
|
||||
|
||||
@@ -280,10 +256,8 @@ class NexusControlPanel:
|
||||
f_f.grid(row=0, column=0, sticky="nsew", padx=(0, 4))
|
||||
b_f, self.back_console = self._create_terminal(bottom_f, " >> SYNAPSE (UVICORN)", text_color="#ffd43b")
|
||||
b_f.grid(row=0, column=1, sticky="nsew", padx=4)
|
||||
mm_f, self.mem_console = self._create_terminal(bottom_f, " >> MEMORY SERVICE", text_color="#78e08f")
|
||||
mm_f.grid(row=0, column=2, sticky="nsew", padx=4)
|
||||
m_f, self.mind_console = self._create_terminal(bottom_f, " >> MINDTRACE", text_color="#cc88ff")
|
||||
m_f.grid(row=0, column=3, sticky="nsew", padx=(4, 0))
|
||||
m_f.grid(row=0, column=2, sticky="nsew", padx=(4, 0))
|
||||
|
||||
def _safe_after(self, delay, func):
|
||||
"""Schedule a Tk callback without ever raising.
|
||||
@@ -305,7 +279,6 @@ class NexusControlPanel:
|
||||
con = self.sys_console
|
||||
if tag == "FRONTEND": con = self.front_console
|
||||
elif tag == "BACKEND": con = self.back_console
|
||||
elif tag == "MEMORY": con = self.mem_console
|
||||
elif tag == "MINDTRACE": con = self.mind_console
|
||||
con.insert(tk.END, f"[{tag}] {message}\n", tag)
|
||||
con.see(tk.END)
|
||||
@@ -358,16 +331,14 @@ class NexusControlPanel:
|
||||
def update_master_ui(self):
|
||||
if self._closing: # window may be destroyed; stop_* still calls this
|
||||
return
|
||||
f, b, m = self._frontend_is_active(), self._backend_is_active(), self._memory_is_active()
|
||||
self.btn_master_start.config(state=tk.DISABLED if (f and b and m) else tk.NORMAL)
|
||||
self.btn_master_stop.config(state=tk.NORMAL if (f or b or m) else tk.DISABLED)
|
||||
f, b = self._frontend_is_active(), self._backend_is_active()
|
||||
self.btn_master_start.config(state=tk.DISABLED if (f and b) else tk.NORMAL)
|
||||
self.btn_master_stop.config(state=tk.NORMAL if (f or b) else tk.DISABLED)
|
||||
self.btn_front_start.config(state=tk.DISABLED if f else tk.NORMAL, bg="#0b3d0b" if f else "#2d2d2d")
|
||||
self.btn_back_start.config(state=tk.DISABLED if b else tk.NORMAL, bg="#3d3d0b" if b else "#2d2d2d")
|
||||
self.btn_mem_start.config(state=tk.DISABLED if m else tk.NORMAL, bg="#0b2d14" if m else "#2d2d2d")
|
||||
self.btn_front_stop.config(state=tk.NORMAL if f else tk.DISABLED)
|
||||
self.btn_front_view.config(state=tk.NORMAL if f else tk.DISABLED)
|
||||
self.btn_back_stop.config(state=tk.NORMAL if b else tk.DISABLED)
|
||||
self.btn_mem_stop.config(state=tk.NORMAL if m else tk.DISABLED)
|
||||
|
||||
def open_frontend_view(self):
|
||||
webbrowser.open_new_tab(FRONTEND_URL)
|
||||
@@ -426,10 +397,6 @@ class NexusControlPanel:
|
||||
self._frontend_starting = False
|
||||
self.frontend_process = p
|
||||
self._write_pid(FRONTEND_PID, p.pid)
|
||||
elif name == "MEMORY":
|
||||
self._memory_starting = False
|
||||
self.memory_process = p
|
||||
self._write_pid(MEMORY_PID, p.pid)
|
||||
else:
|
||||
self._backend_starting = False
|
||||
self.backend_process = p
|
||||
@@ -472,10 +439,6 @@ class NexusControlPanel:
|
||||
self._frontend_starting = False
|
||||
self.frontend_process = None
|
||||
FRONTEND_PID.unlink(missing_ok=True)
|
||||
elif name == "MEMORY":
|
||||
self._memory_starting = False
|
||||
self.memory_process = None
|
||||
MEMORY_PID.unlink(missing_ok=True)
|
||||
else:
|
||||
self._backend_starting = False
|
||||
self.backend_process = None
|
||||
@@ -576,47 +539,13 @@ class NexusControlPanel:
|
||||
self._terminate_pid_file(BACKEND_PID)
|
||||
self.update_master_ui()
|
||||
|
||||
def start_memory(self):
|
||||
if self._memory_is_active():
|
||||
return
|
||||
self._memory_starting = True
|
||||
self.update_master_ui()
|
||||
self.log("SYSTEM", "Starting MEMORY SERVICE...")
|
||||
threading.Thread(target=self._worker, args=("MEMORY", MEMORY_CMD, MEMORY_DIR), daemon=True).start()
|
||||
|
||||
def stop_memory(self):
|
||||
if self.memory_process:
|
||||
self._terminate_process(self.memory_process)
|
||||
self.memory_process = None
|
||||
elif self._is_running(MEMORY_PID):
|
||||
self._terminate_pid_file(MEMORY_PID)
|
||||
self.update_master_ui()
|
||||
|
||||
def _memory_is_active(self):
|
||||
return self._memory_starting or self.memory_process is not None or self._is_running(MEMORY_PID)
|
||||
|
||||
def _monitor_memory(self):
|
||||
log_path = PROJECT_ROOT / "runtime/memory.log"
|
||||
try:
|
||||
with open(log_path, "r") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
while self._is_running(MEMORY_PID):
|
||||
line = f.readline()
|
||||
if line:
|
||||
self.log("MEMORY", line.rstrip())
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
self.log("ERROR", f"MEMORY monitor fault: {e}")
|
||||
|
||||
def start_all(self):
|
||||
self.start_memory()
|
||||
self.start_frontend()
|
||||
self.start_backend()
|
||||
|
||||
def kill_all(self):
|
||||
self.log("SYSTEM", "FORCE KILL — terminating all Nexus processes by port...")
|
||||
ports = {8000: "SYNAPSE", 8001: "MEMORY", 5173: "INTERFACE"}
|
||||
ports = {8000: "SYNAPSE", 5173: "INTERFACE"}
|
||||
for port, name in ports.items():
|
||||
try:
|
||||
r = subprocess.run(["fuser", "-k", f"{port}/tcp"], capture_output=True, timeout=5)
|
||||
@@ -628,21 +557,18 @@ class NexusControlPanel:
|
||||
subprocess.run(["pkill", "-9", "-f", pat], capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
for pid_file in [FRONTEND_PID, BACKEND_PID, MEMORY_PID]:
|
||||
for pid_file in [FRONTEND_PID, BACKEND_PID]:
|
||||
pid_file.unlink(missing_ok=True)
|
||||
self.frontend_process = None
|
||||
self.backend_process = None
|
||||
self.memory_process = None
|
||||
self._frontend_starting = False
|
||||
self._backend_starting = False
|
||||
self._memory_starting = False
|
||||
self._safe_after(0, self.update_master_ui)
|
||||
self.log("SYSTEM", "Force kill complete.")
|
||||
|
||||
def stop_all(self):
|
||||
self.stop_frontend()
|
||||
self.stop_backend()
|
||||
self.stop_memory()
|
||||
|
||||
def on_close(self):
|
||||
if self._closing:
|
||||
@@ -675,13 +601,13 @@ class NexusControlPanel:
|
||||
except Exception:
|
||||
pass
|
||||
# Services the panel spawned itself (each in its own setsid group):
|
||||
for proc in (self.frontend_process, self.backend_process, self.memory_process):
|
||||
for proc in (self.frontend_process, self.backend_process):
|
||||
if proc:
|
||||
self._terminate_process(proc)
|
||||
# Services attached via pid file (ncp-started, one shared group);
|
||||
# _terminate_pid_file verifies ownership and unlinks. Guarantee the
|
||||
# unlink even for a pid that's already dead so no stale files remain.
|
||||
for pf in (FRONTEND_PID, BACKEND_PID, MEMORY_PID):
|
||||
for pf in (FRONTEND_PID, BACKEND_PID):
|
||||
if self._is_running(pf):
|
||||
self._terminate_pid_file(pf)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user