Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6ecd510
Fix TUI bugs and UX issues from real hardware testing
eshork Apr 24, 2026
334ef93
Show model metadata and fix button visibility in model list
eshork Apr 24, 2026
927df1a
Save API key to persistent disk alongside overlay
eshork Apr 24, 2026
a1e82c4
Add live clock to dashboard top-right corner
eshork Apr 24, 2026
bdfd694
Fix chat screen layout and text wrapping
eshork Apr 24, 2026
b9edad1
Preserve selected model when returning to chat screen
eshork Apr 24, 2026
d0f1ca8
Add model delete and fix chat model persistence
eshork Apr 24, 2026
ff34cab
Harden wizard finalization, add --wizard flag, and Enter-to-pull
eshork Apr 24, 2026
fbac7b6
Harden partition detection, wizard source of truth, and subprocess er…
eshork Apr 24, 2026
6efe83a
Move partition snapshot before mkpart to prevent race condition
eshork Apr 24, 2026
5534f54
Harden partition creation safety and boot device detection
eshork Apr 24, 2026
c0e802c
Guard pull button and Enter against concurrent submissions
eshork Apr 24, 2026
b0d8a88
Remove dual wizard marker, check all subprocess returns, normalize li…
eshork Apr 24, 2026
64a9514
Fall through to findmnt when live-media PKNAME fails
eshork Apr 24, 2026
b1003b1
Fix Header crash on screen transitions and simplify --wizard flag
eshork Apr 24, 2026
493fe4e
Fix GPU acceleration: load nvidia-uvm at boot and remove cgroup devic…
eshork Apr 24, 2026
5e1d376
Escape Rich markup in [GPU]/[CPU] tags so they render visibly
eshork Apr 24, 2026
5f8908e
Add arrow-key navigation with scroll-follow to installed models list
eshork Apr 24, 2026
b555d1f
Unify models screen focus: zone-based Tab, arrow-key list+button nav
eshork Apr 24, 2026
3c12f7d
Models screen: skip disabled buttons, Loading... feedback, column legend
eshork Apr 24, 2026
8064d81
Restore _unload_from_vram and add legend column separators
eshork Apr 24, 2026
fe0a28f
Fix unload race condition and keep manually loaded models in VRAM
eshork Apr 24, 2026
4bc2f32
Fix keep_alive: pass integer -1 instead of string
eshork Apr 24, 2026
78fbc0d
Redesign services screen to match models screen UX
eshork Apr 24, 2026
c8e3a71
Remap screen hotkeys to F1-F5: Dash, Models, Svc, Logs, Chat
eshork Apr 24, 2026
ea9fcfc
Guard service poll timer against widget rebuild race
eshork Apr 24, 2026
2a704c6
Allow concurrent model loading and persist Ollama config
eshork Apr 24, 2026
37d0330
Widen services Restart button to fit label
eshork Apr 24, 2026
2160fa8
Create webui data directory on persistence partition
eshork Apr 24, 2026
7692b40
fix git urls
eshork Apr 24, 2026
5dec79c
Update documentation to reflect TUI redesign, GPU fixes, and config c…
eshork Apr 24, 2026
c621827
Mark VRAM-loaded models with * in chat selector and retain input focus
eshork Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ NeuralDrive images are built using Debian's `live-build` toolchain inside a Dock

```bash
# Clone and build
git clone https://github.com/NeuralDrive/NeuralDrive.git
git clone https://github.com/Rightbracket/NeuralDrive.git
cd NeuralDrive
docker compose run --rm builder

Expand Down
6 changes: 6 additions & 0 deletions config/hooks/live/01-setup-system.chroot
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ echo "neuraldrive-admin:neuraldrive" | chpasswd
echo "neuraldrive-admin ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/neuraldrive-admin
chmod 440 /etc/sudoers.d/neuraldrive-admin

# Scoped NOPASSWD rules for TUI — survives wizard _finalize() which only
# modifies neuraldrive-admin. File is baked in via includes.chroot; just
# ensure correct ownership and permissions here.
chmod 440 /etc/sudoers.d/neuraldrive-tui
chown root:root /etc/sudoers.d/neuraldrive-tui

mkdir -p /etc/neuraldrive/tls \
/var/lib/neuraldrive/models/{manifests,blobs} \
/var/lib/neuraldrive/ollama \
Expand Down
4 changes: 2 additions & 2 deletions config/hooks/live/04-install-python-apps.chroot
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ git clone --depth 1 https://github.com/psalias2006/gpu-hot.git /usr/lib/neuraldr
# --- TUI (terminal interface) ---
python3 -m venv /usr/lib/neuraldrive/tui/venv
/usr/lib/neuraldrive/tui/venv/bin/pip install --no-cache-dir --upgrade pip
/usr/lib/neuraldrive/tui/venv/bin/pip install --no-cache-dir textual psutil httpx rich
/usr/lib/neuraldrive/tui/venv/bin/pip install --no-cache-dir textual psutil httpx rich pyyaml

cat > /usr/local/bin/neuraldrive-tui << 'LAUNCHER'
#!/bin/sh
exec /usr/lib/neuraldrive/tui/venv/bin/python /usr/lib/neuraldrive/tui/main.py
exec /usr/lib/neuraldrive/tui/venv/bin/python /usr/lib/neuraldrive/tui/main.py "$@"
LAUNCHER
chmod +x /usr/local/bin/neuraldrive-tui

Expand Down
2 changes: 1 addition & 1 deletion config/hooks/live/05-generate-configs.chroot
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ cat > /etc/neuraldrive/ollama.conf << 'EOF'
OLLAMA_HOST=127.0.0.1:11434
OLLAMA_MODELS=/var/lib/neuraldrive/models/
OLLAMA_KEEP_ALIVE=5m
OLLAMA_MAX_LOADED_MODELS=1
OLLAMA_MAX_LOADED_MODELS=0
OLLAMA_NUM_PARALLEL=1
EOF

Expand Down
4 changes: 4 additions & 0 deletions config/includes.chroot/etc/modules-load.d/nvidia-uvm.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Load NVIDIA Unified Virtual Memory module at boot.
# Required for CUDA GPU memory allocation (Ollama inference).
# Harmless on systems without NVIDIA GPUs (modprobe fails silently).
nvidia-current-uvm
2 changes: 1 addition & 1 deletion config/includes.chroot/etc/neuraldrive/ollama.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
OLLAMA_HOST=127.0.0.1:11434
OLLAMA_MODELS=/var/lib/neuraldrive/models/
OLLAMA_KEEP_ALIVE=5m
OLLAMA_MAX_LOADED_MODELS=1
OLLAMA_MAX_LOADED_MODELS=0
OLLAMA_NUM_PARALLEL=1
30 changes: 30 additions & 0 deletions config/includes.chroot/etc/sudoers.d/neuraldrive-tui
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Scoped NOPASSWD rules for NeuralDrive TUI operations.
# This file is NOT modified by the first-boot wizard's _finalize()
# (which only touches /etc/sudoers.d/neuraldrive-admin).
# Processed AFTER neuraldrive-admin (alphabetical), so these NOPASSWD
# rules override the password-required ALL rule for matched commands.

# Service management
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/systemctl start neuraldrive-*
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop neuraldrive-*
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart neuraldrive-*
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active neuraldrive-*

# Partition creation and storage management
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/sbin/parted *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/sbin/mkfs.ext4 *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/mount *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/umount *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/sbin/partprobe *

# File operations (wizard config writing, directory setup)
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/tee *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/mkdir *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/chmod *
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/chown *

# Password management (wizard security step)
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/sbin/chpasswd

# Sudoers self-read (wizard _finalize reads neuraldrive-admin to strip NOPASSWD)
neuraldrive-admin ALL=(ALL) NOPASSWD: /usr/bin/cat /etc/sudoers.d/neuraldrive-admin
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ Requires=neuraldrive-gpu-detect.service
[Service]
Environment=HOME=/var/lib/neuraldrive/ollama
EnvironmentFile=/etc/neuraldrive/ollama.conf
EnvironmentFile=-/var/lib/neuraldrive/config/ollama.conf
ExecStartPre=/usr/bin/mkdir -p /var/lib/neuraldrive/models
ExecStartPre=-/sbin/modprobe nvidia-current-uvm
ExecStartPre=-/usr/bin/nvidia-modprobe -u
ExecStart=/usr/local/bin/ollama serve
User=neuraldrive-ollama
Group=neuraldrive-ollama
Expand All @@ -26,8 +29,6 @@ PrivateTmp=yes
PrivateDevices=no
ProtectKernelTunables=yes
ProtectControlGroups=yes
DeviceAllow=/dev/nvidia* rw
DeviceAllow=/dev/dri/* rw
ReadWritePaths=/var/lib/neuraldrive /var/log/neuraldrive /etc/neuraldrive /run/neuraldrive

[Install]
Expand Down
38 changes: 38 additions & 0 deletions config/includes.chroot/usr/lib/neuraldrive/dev-reset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# /usr/lib/neuraldrive/dev-reset.sh
# Development reset script — restores a post-wizard system to a
# development-friendly state. Included in builds for convenience.
#
# Usage: sudo /usr/lib/neuraldrive/dev-reset.sh

set -e

echo "=== NeuralDrive Development Reset ==="
echo ""

# 1. Reset admin password to the build default
echo "neuraldrive-admin:neuraldrive" | chpasswd
echo "[ok] Admin password reset to 'neuraldrive'"

# 2. Restore blanket NOPASSWD for development
echo "neuraldrive-admin ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/neuraldrive-admin
chmod 440 /etc/sudoers.d/neuraldrive-admin
echo "[ok] Blanket NOPASSWD sudo restored"

# 3. Remove wizard sentinel so it runs again on next TUI start
rm -f /etc/neuraldrive/first-boot-complete
echo "[ok] Wizard sentinel removed"

# 4. Clear config files so wizard starts fresh
rm -f /var/lib/neuraldrive/config/config.yaml
rm -f /etc/neuraldrive/config.yaml
echo "[ok] Config files cleared"

# 5. Clear generated credentials
rm -f /etc/neuraldrive/api.key
rm -f /etc/neuraldrive/credentials.conf
echo "[ok] API key and credentials cleared"

echo ""
echo "Development reset complete."
echo "Restart the TUI to re-run the first-boot wizard."
101 changes: 93 additions & 8 deletions config/includes.chroot/usr/lib/neuraldrive/tui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,74 @@
from screens.chat import ChatScreen
from screens.wizard import FirstBootWizard

import argparse
import os
import subprocess
import sys
import traceback
from datetime import datetime

PERSIST_DIR = "/var/lib/neuraldrive"
OVERLAY_LOG_DIR = "/var/log/neuraldrive"


def _persistent_available() -> bool:
return os.path.ismount(PERSIST_DIR)


def _log_dir() -> str:
if _persistent_available():
p = os.path.join(PERSIST_DIR, "logs")
try:
os.makedirs(p, exist_ok=True)
return p
except PermissionError:
pass
os.makedirs(OVERLAY_LOG_DIR, exist_ok=True)
return OVERLAY_LOG_DIR


def _screenshot_dir() -> str:
if _persistent_available():
p = os.path.join(PERSIST_DIR, "screenshots")
try:
os.makedirs(p, exist_ok=True)
return p
except PermissionError:
pass
os.makedirs(OVERLAY_LOG_DIR, exist_ok=True)
return OVERLAY_LOG_DIR


def _write_crash_dump(error: BaseException) -> str | None:
try:
crash_dir = _log_dir()
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
dump_path = os.path.join(crash_dir, f"tui-crash-{ts}.log")
with open(dump_path, "w") as f:
f.write(f"NeuralDrive TUI crash at {ts}\n")
f.write(f"Python: {sys.version}\n")
f.write(f"Args: {sys.argv}\n\n")
traceback.print_exception(type(error), error, error.__traceback__, file=f)
return dump_path
except Exception:
return None


class NeuralDriveTUI(App):
CSS_PATH = "styles.tcss"
TITLE = "NeuralDrive"
ENABLE_COMMAND_PALETTE = False

BINDINGS = [
Binding("m", "switch_screen('models')", "Models"),
Binding("s", "switch_screen('services')", "Services"),
Binding("n", "switch_screen('network')", "Network"),
Binding("l", "switch_screen('logs')", "Logs"),
Binding("c", "switch_screen('chat')", "Chat"),
Binding("d", "switch_screen('dashboard')", "Dashboard"),
Binding("f1", "switch_screen('dashboard')", "F1 Dash", priority=True),
Binding("f2", "switch_screen('models')", "F2 Models", priority=True),
Binding("f3", "switch_screen('services')", "F3 Svc", priority=True),
Binding("f4", "switch_screen('logs')", "F4 Logs", priority=True),
Binding("f5", "switch_screen('chat')", "F5 Chat", priority=True),
Binding("q", "quit", "Quit"),
Binding("up", "focus_previous", "Previous", show=False),
Binding("down", "focus_next", "Next", show=False),
]

SCREENS = {
Expand All @@ -40,11 +93,43 @@ def on_mount(self) -> None:
if not os.path.exists("/etc/neuraldrive/first-boot-complete"):
self.push_screen(FirstBootWizard())

def _handle_exception(self, error: Exception) -> None:
dump_path = _write_crash_dump(error)
if dump_path:
self.log(f"Crash dump saved to {dump_path}")
super()._handle_exception(error)

def action_focus_next(self) -> None:
self.screen.focus_next()

def action_focus_previous(self) -> None:
self.screen.focus_previous()

def action_switch_screen(self, screen_name: str) -> None:
if screen_name in self.SCREENS:
self.switch_screen(screen_name)


if __name__ == "__main__":
app = NeuralDriveTUI()
app.run(mouse=False)
parser = argparse.ArgumentParser(description="NeuralDrive TUI")
parser.add_argument(
"--wizard", action="store_true", help="Force the first-boot wizard to run"
)
args = parser.parse_args()

if args.wizard:
sentinel = "/etc/neuraldrive/first-boot-complete"
if os.path.exists(sentinel):
subprocess.run(["sudo", "rm", "-f", sentinel], timeout=5)

screenshot_dir = _screenshot_dir()
os.environ["TEXTUAL_SCREENSHOT_LOCATION"] = screenshot_dir
try:
app = NeuralDriveTUI()
app.run(mouse=False)
except Exception as exc:
dump_path = _write_crash_dump(exc)
traceback.print_exc()
if dump_path:
print(f"\nCrash dump saved to {dump_path}")
sys.exit(1)
Loading
Loading