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:
janvanwan
2026-08-25 09:13:55 -05:00
parent fe5d18afa7
commit 42eaed647a
88 changed files with 4280 additions and 1888 deletions
+15 -89
View File
@@ -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: