diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..920c08b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +# Nothing is COPY'd into the image — the Dockerfile clones from GitHub. +# This file is kept as a safeguard in case a COPY is added later. + +# Secrets — never bake certs or keys into the image +secrets/ + +# OS +.DS_Store +Thumbs.db diff --git a/.gitignore b/.gitignore index d4bb158..9a4722b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ experiments/ # Local databases NAS_Database/ Django_database/ +.claudeignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..05fc2ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,87 @@ +# ============================================================ +# SpikesortingLabHub — Production Image +# +# Self-contained: downloads the repo from GitHub so anyone can +# reproduce this image without needing the local source tree. +# ============================================================ + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# ------------------------------------------------------------ +# 1. System packages +# ------------------------------------------------------------ +RUN apt-get update && apt-get upgrade -y --no-install-recommends +RUN apt-get install -y --no-install-recommends \ + python3 \ + python3-venv \ + python3-pip \ + build-essential \ + libpq-dev \ + openssl \ + ca-certificates \ + curl \ + git \ + nodejs \ + npm \ + wget \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +# ------------------------------------------------------------ +# 2. Download repository (docker branch) and unpack +# ------------------------------------------------------------ +WORKDIR /app +RUN wget -q https://github.com/UserFriendlySpikesorting/SpikesortingLabHub-server/archive/refs/heads/developing_branch.zip \ + && unzip -q developing_branch.zip \ + && mv SpikesortingLabHub-server-developing_branch/* . \ + && rm -rf developing_branch.zip SpikesortingLabHub-server-developing_branch + +# ------------------------------------------------------------ +# 3. Set up Python virtual environment and install requirements +# ------------------------------------------------------------ +RUN python3 -m venv /app/venv +RUN /app/venv/bin/pip install --no-cache-dir -r requirements.txt + +# Install SpikesortingLabHub-CLI (patch setup.py for Python 3.12) +RUN wget https://github.com/UserFriendlySpikesorting/SpikesortingLabHub-CLI/archive/refs/heads/main.zip \ + && unzip main.zip \ + && sed -i 's|3.13|3.12|' SpikesortingLabHub-CLI-main/setup.py \ + && /app/venv/bin/pip install --no-cache-dir SpikesortingLabHub-CLI-main/ + +ENV PATH="/app/venv/bin:$PATH" + +# ------------------------------------------------------------ +# 4. Build the React frontend +# my-app/build/ is gitignored so it must be compiled here. +# ------------------------------------------------------------ +RUN cd my-app && npm ci --omit=dev && npm run build + +# ------------------------------------------------------------ +# 5. Placeholder directories for bind mounts +# Docker Compose overlays the real host paths at runtime. +# /data ← trurnasdata (read-only NAS database) +# /django_db ← persistentdata (Django SQLite DB + logs) +# /experiments ← binary recording files (read-only) +# ------------------------------------------------------------ +RUN mkdir -p /data /app/django_db /app/experiments /app/secrets + +# ------------------------------------------------------------ +# 6. Entrypoint — already in the repo after download. +# Runs pre-flight checks, collectstatic, migrate, then Gunicorn. +# ------------------------------------------------------------ +RUN chmod +x /app/entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] + +# ------------------------------------------------------------ +# 7. Expose ports +# 9000 — plain HTTP +# 9443 — HTTPS (Gunicorn with --certfile / --keyfile) +# ------------------------------------------------------------ +EXPOSE 9000 9443 + +# ------------------------------------------------------------ +# 8. Default command — passed to entrypoint.sh via exec "$@". +# ------------------------------------------------------------ +CMD ["gunicorn", "-c", "gunicorn.conf.py", "labhub.wsgi:application"] diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..92be5ac --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,108 @@ +# SpikesortingLabHub — TrueNAS Deployment Instructions + +Steps tested on the test rig. Replicate these exactly on the main TrueNAS. + +> **Path difference:** test rig used `user_home`, main TrueNAS uses `users`. +> Every path below already reflects the main TrueNAS convention. + +--- + +## 1. Enable SSH on TrueNAS + +TrueNAS UI → System → Services → start **SSH** and set it to auto-start. + +--- + +## 2. Copy the init script from your Mac to TrueNAS + +Run from your Mac terminal (not SSH): + +```bash +scp /Users/kajalpatel/SpikesortingLabHub-server/truenas_init.sh \ + kajal@128.164.33.182:/mnt/root_data_storage/users/kajal/ +``` + +--- + +## 3. SSH into TrueNAS + +```bash +ssh truenas_admin@128.164.33.182 +``` + +--- + +## 4. Move the script into the sslh folder + +```bash +sudo cp /mnt/root_data_storage/users/kajal/truenas_init.sh \ + /mnt/root_data_storage/users/sslh/truenas_init.sh + +sudo chmod +x /mnt/root_data_storage/users/sslh/truenas_init.sh +``` + +--- + +## 5. Test-run the script manually + +```bash +sudo /mnt/root_data_storage/users/sslh/truenas_init.sh +``` + +Check the log it generates: + +```bash +cat /mnt/root_data_storage/users/sslh/sslh_init.log +``` + +--- + +## 6. Pull the Docker image and start the container (first time only) + +```bash +docker pull ikajalpatel21/spikesorting-labhub-latestimg:latest + +export DJANGO_SECRET_KEY="$(cat /mnt/root_data_storage/users/sslh/secrets/django_secret.key)" + +docker compose -f /mnt/root_data_storage/users/sslh/docker-compose.yml up -d +``` + +> If `secrets/django_secret.key` does not exist yet, generate it first: +> ```bash +> mkdir -p /mnt/root_data_storage/users/sslh/secrets +> openssl rand -hex 50 > /mnt/root_data_storage/users/sslh/secrets/django_secret.key +> chmod 600 /mnt/root_data_storage/users/sslh/secrets/django_secret.key +> ``` + +--- + +## 7. Verify the container is running + +```bash +docker ps +docker logs spikesorting-labhub-server-spikesorting-labhub-1 +``` + +Open in browser: `https://128.164.33.182:9443` +(self-signed cert warning is expected — click through) + +--- + +## 8. Register the init script in TrueNAS UI + +System → Advanced Settings → Init/Shutdown Scripts → **Add** + +| Field | Value | +|---------|---------------------------------------------------------| +| Type | Script | +| Script | `/mnt/root_data_storage/users/sslh/truenas_init.sh` | +| When | Pre Init | +| Timeout | 30 | + +--- + +## What happens on every reboot after this + +1. TrueNAS kernel starts → ZFS pool mounts automatically +2. Pre Init script runs → verifies/creates bind-mount directories → bind-mounts experiments and trurnasdata +3. Docker daemon starts → `restart: unless-stopped` brings the container back up with all mounts in place diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 3cdaf51..0000000 --- a/deploy.sh +++ /dev/null @@ -1,299 +0,0 @@ -#!/bin/bash - -# ============================================================================= -# QModel Django Deployment Script -# ============================================================================= -# This script sets up the complete environment for the qmodel branch including: -# - GitHub repository setup -# - Virtual environment creation -# - Dependencies installation -# - Database initialization -# - SSL certificate generation -# - Static files collection -# - Server startup options -# ============================================================================= - -set -e # Exit on any error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -REPO_URL="https://github.com/iKajalpatel21/spikesorting-labhub-try-error.git" -BRANCH_NAME="qmodel" -PROJECT_DIR="spikesorting-labhub-try-error" -VENV_NAME=".djangovenv" - -# Function to print colored output -print_status() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Function to check if command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# ============================================================================= -# 1. System Requirements Check -# ============================================================================= -print_status "Checking system requirements..." - -if ! command_exists python3; then - print_error "Python3 is not installed. Please install Python3 first." - exit 1 -fi - -if ! command_exists git; then - print_error "Git is not installed. Please install Git first." - exit 1 -fi - -if ! command_exists openssl; then - print_error "OpenSSL is not installed. Please install OpenSSL first." - exit 1 -fi - -print_success "All system requirements are met." - -# ============================================================================= -# 2. Repository Setup -# ============================================================================= -print_status "Setting up repository..." - -# Check if we're already in the project directory -if [[ $(basename "$PWD") == "$PROJECT_DIR" ]]; then - print_status "Already in project directory. Pulling latest changes..." - git fetch origin - git checkout $BRANCH_NAME || git checkout -b $BRANCH_NAME origin/$BRANCH_NAME - git pull origin $BRANCH_NAME -else - # Check if project directory exists - if [ -d "$PROJECT_DIR" ]; then - print_status "Project directory exists. Updating..." - cd "$PROJECT_DIR" - git fetch origin - git checkout $BRANCH_NAME || git checkout -b $BRANCH_NAME origin/$BRANCH_NAME - git pull origin $BRANCH_NAME - else - print_status "Cloning repository..." - git clone -b $BRANCH_NAME $REPO_URL $PROJECT_DIR - cd "$PROJECT_DIR" - fi -fi - -print_success "Repository setup complete." - -# ============================================================================= -# 3. Virtual Environment Setup -# ============================================================================= -print_status "Setting up Python virtual environment..." - -# Create virtual environment if it doesn't exist -if [ ! -d "$VENV_NAME" ]; then - python3 -m venv $VENV_NAME || { - print_error "Failed to create virtual environment" - exit 1 - } -fi - -# Activate virtual environment -source $VENV_NAME/bin/activate || { - print_error "Failed to activate virtual environment" - exit 1 -} - -print_success "Virtual environment activated." - -# ============================================================================= -# 4. Dependencies Installation -# ============================================================================= -print_status "Installing/updating dependencies..." - -# Update pip -pip install -U pip || { - print_error "Failed to update pip" - exit 1 -} - -# Install requirements -if [ -f "requirements.txt" ]; then - pip install -U -r requirements.txt || { - print_error "Failed to install requirements from requirements.txt" - exit 1 - } -else - # Fallback to manual installation - print_warning "requirements.txt not found. Installing core dependencies manually..." - pip install -U django djangorestframework requests gunicorn urllib3 || { - print_error "Failed to install core dependencies" - exit 1 - } -fi - -print_success "Dependencies installed successfully." - -# ============================================================================= -# 5. Database Setup -# ============================================================================= -print_status "Setting up database..." - -# Ask user if they want to reset the database -read -p "Do you want to reset the database? This will clear all existing data. (y/N): " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - print_warning "Resetting database..." - echo -n > db.sqlite3 -fi - -# Run migrations -print_status "Running database migrations..." -python manage.py makemigrations || { - print_error "Failed to create migrations" - exit 1 -} - -python manage.py migrate || { - print_error "Failed to run migrations" - exit 1 -} - -print_success "Database setup complete." - -# ============================================================================= -# 6. Create Superuser (Optional) -# ============================================================================= -read -p "Do you want to create a superuser? (y/N): " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - print_status "Creating superuser..." - python manage.py createsuperuser --username admin || { - print_warning "Superuser creation failed or was skipped" - } -fi - -# ============================================================================= -# 7. SSL Certificate Generation -# ============================================================================= -print_status "Checking SSL certificates..." - -mkdir -p secrets -if [ ! -f "secrets/cert.crt" ] || [ ! -f "secrets/cert.key" ]; then - print_status "Generating self-signed SSL certificate..." - # Use the machine's actual IP/hostname as CN so clients can verify it. - # SAN (subjectAltName) covers both the hostname and IP address. - SERVER_HOST=$(hostname -f 2>/dev/null || hostname) - SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}') - print_status "Certificate CN: ${SERVER_HOST} (IP: ${SERVER_IP})" - - openssl req -x509 -newkey rsa:4096 -keyout secrets/cert.key -out secrets/cert.crt -days 365 -nodes \ - -subj "/CN=${SERVER_HOST}" \ - -addext "subjectAltName=DNS:${SERVER_HOST},IP:${SERVER_IP},DNS:localhost,IP:127.0.0.1" || { - print_error "Failed to generate SSL certificates" - exit 1 - } - print_success "SSL certificate generated: secrets/cert.crt / secrets/cert.key (CN=${SERVER_HOST})" -else - print_success "SSL certificates already exist (secrets/cert.crt / secrets/cert.key)." -fi - -# ============================================================================= -# 8. Static Files Collection -# ============================================================================= -print_status "Collecting static files..." -yes yes | python manage.py collectstatic || { - print_warning "Static files collection failed or was skipped" -} - -print_success "Static files collected." - -# ============================================================================= -# 9. Server Startup Options -# ============================================================================= -print_success "Deployment complete! Choose how to run the server:" -echo -echo "Available options:" -echo "1) Development server (HTTP on port 8000)" -echo "2) Gunicorn server (HTTP on port 8000)" -echo "3) Gunicorn server with HTTPS (port 8443)" -echo "4) Just setup - don't start server" -echo "5) Start worker only" -echo - -read -p "Enter your choice (1-5): " -n 1 -r -echo - -case $REPLY in - 1) - print_status "Starting Django development server..." - python manage.py runserver - ;; - 2) - print_status "Starting Gunicorn HTTP server..." - gunicorn -c gunicorn.conf.py labhub.wsgi:application - ;; - 3) - print_status "Starting Gunicorn HTTPS server (port 443)..." - gunicorn -c gunicorn.conf.py \ - --certfile=secrets/cert.crt \ - --keyfile=secrets/cert.key \ - -b 0.0.0.0:443 \ - labhub.wsgi:application - ;; - 4) - print_success "Setup complete. You can manually start the server when ready." - echo - echo "To start the development server: python manage.py runserver" - echo "To start Gunicorn HTTP: gunicorn -c gunicorn.conf.py labhub.wsgi:application" - echo "To start Gunicorn HTTPS: gunicorn -c gunicorn.conf.py --certfile=secrets/cert.crt --keyfile=secrets/cert.key -b 0.0.0.0:443 labhub.wsgi:application" - echo "To start worker: python qmodel_worker.py" - ;; - 5) - print_status "Starting qmodel worker..." - python qmodel_worker.py - ;; - *) - print_warning "Invalid choice. Setup complete but no server started." - ;; -esac - -# ============================================================================= -# 10. Final Instructions -# ============================================================================= -echo -print_success "=== Deployment Summary ===" -echo "Project: $PROJECT_DIR" -echo "Branch: $BRANCH_NAME" -echo "Virtual Environment: $VENV_NAME" -echo "Database: SQLite (db.sqlite3)" -echo "SSL Certificates: cert.pem, key.pem" -echo -echo "=== Usage Instructions ===" -echo "• HTTP — Django admin: http://localhost:8000/admin/" -echo "• HTTPS — Django admin: https://localhost:443/admin/" -echo "• HTTP — Worker fetch: http://localhost:8000/job-queue/next-job/" -echo "• HTTPS — Worker fetch: https://localhost:443/job-queue/next-job/" -echo "• Worker (HTTP): python qmodel_worker.py" -echo "• Worker (HTTPS): LABHUB_BASE_URL=https://localhost LABHUB_SSL_VERIFY=false python qmodel_worker.py" -echo -echo "=== Multiple Terminal Setup ===" -echo "Terminal 1 (Server): ./deploy.sh (choose option 2 or 3)" -echo "Terminal 2 (Worker): source $VENV_NAME/bin/activate && python qmodel_worker.py" -echo -print_success "Setup complete! Enjoy your deployment and run the server as needed." diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c06e25f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +# ============================================================ +# SpikesortingLabHub — Docker Compose +# +# Bind-mount layout (host path → container path): +# +# trurnasdata /mnt/root_data_storage/users/sslh/trurnasdata → /data (read-only — NAS database) +# persistentdata /mnt/root_data_storage/users/sslh/persistentdata → /django_db (read/write — Django DB + logs) +# experiments /mnt/root_data_storage/experiments → /experiments (read-only — binary recordings) +# +# Ports 8000, 8080, and 8443 are used by the NAS itself — avoid them. +# +# Usage: +# export DJANGO_SECRET_KEY="your-secret-key" +# docker compose build +# docker compose up -d +# (entrypoint.sh generates SSL certs automatically on first start) +# ============================================================ + +services: + spikesorting-labhub: + build: . + image: spikesorting-labhub:latest + restart: unless-stopped + + command: + - gunicorn + - -c + - gunicorn.conf.py + - --certfile=/app/secrets/cert.crt + - --keyfile=/app/secrets/cert.key + - -b + - 0.0.0.0:9443 + - labhub.wsgi:application + + ports: + - "9443:9443" # HTTPS — Gunicorn binds here via command: override above + + environment: + # Django core + DJANGO_SECRET_KEY: "${DJANGO_SECRET_KEY}" + DJANGO_DEBUG: "False" + + # Database lives on the persistentdata mount + DATABASE_PATH: "/django_db/db.sqlite3" + + # Tell Django where the NAS root is so $NAS$ placeholders resolve + NAS_ROOT: "/data" + + # Scan these directories for .bin / .prb data files + DATA_DIRS: "/experiments,/experiments/probes" + + volumes: + # trurnasdata — NAS database (read-only) + - type: bind + source: /mnt/root_data_storage/users/sslh/trurnasdata + target: /data + read_only: true + + # persistentdata — Django SQLite DB + logs (read/write) + - type: bind + source: /mnt/root_data_storage/users/sslh/persistentdata + target: /django_db + + # experiments — binary recording files (read-only) + - type: bind + source: /mnt/root_data_storage/experiments + target: /experiments + read_only: true + + # secrets — SSL certs generated by entrypoint.sh on first start (read-write) + - type: bind + source: /mnt/root_data_storage/users/sslh/secrets + target: /app/secrets diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..089125f --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +ok() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +info() { echo "$1"; } + + +# ============================================================================= +# 1. Check the database directory (mounted from persistentdata on the host) +# ============================================================================= +DB_PATH="${DATABASE_PATH:-/app/django_db/db.sqlite3}" +DB_DIR="$(dirname "$DB_PATH")" + +# mkdir -p "$DB_DIR" + +if [ -f "$DB_PATH" ]; then + ok "SQLite database exists: $DB_PATH" +else + warn "SQLite database not found: $DB_PATH" + info "Creating SQLite DB via Django migrations..." + + python manage.py migrate --noinput + + ok "SQLite database created and migrations applied." +fi + +# ============================================================================= +# 2. Generate SSL certificate if not already present +# ============================================================================= +# mkdir -p /app/secrets + +if [ -f "/app/secrets/cert.crt" ] && [ -f "/app/secrets/cert.key" ]; then + ok "SSL certificates already exist — leaving untouched." +else + echo "Generating self-signed TLS certificate into /app/secrets/..." + + SERVER_HOST=$(hostname -f 2>/dev/null || hostname) + SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + [ -z "$SERVER_IP" ] && SERVER_IP="127.0.0.1" + + openssl req -x509 -newkey rsa:4096 \ + -keyout /app/secrets/cert.key \ + -out /app/secrets/cert.crt \ + -days 365 -nodes \ + -subj "/CN=${SERVER_HOST}" \ + -addext "subjectAltName=DNS:${SERVER_HOST},IP:${SERVER_IP},DNS:localhost,IP:127.0.0.1" + + ok "Certificate created (CN=${SERVER_HOST}, IP=${SERVER_IP})" +fi + +# ============================================================================= +# 3. Collect static files — needs DJANGO_SECRET_KEY from the environment. +# ============================================================================= +python manage.py collectstatic --noinput + +# ============================================================================= +# 4. Run database migrations — safe to run repeatedly. +# ============================================================================= +python manage.py migrate --noinput + +# ============================================================================= +# 5. Hand off to the main process (Gunicorn). +# ============================================================================= +exec "$@" diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 932bc8e..0b739de 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -6,7 +6,7 @@ Usage: HTTP: gunicorn -c gunicorn.conf.py labhub.wsgi:application - HTTPS: gunicorn -c gunicorn.conf.py --certfile=secrets/cert.crt --keyfile=secrets/cert.key -b 0.0.0.0:443 labhub.wsgi:application + HTTPS: gunicorn -c gunicorn.conf.py --certfile=cert.crt --keyfile=cert.key -b 0.0.0.0:9443 labhub.wsgi:application On Linux, binding to port 443 requires one of: sudo gunicorn ... @@ -18,7 +18,7 @@ # ----------------------------------------------------------------------------- # Server socket (overridden by -b on the command line when using HTTPS) # ----------------------------------------------------------------------------- -bind = "0.0.0.0:8000" +bind = "0.0.0.0:9000" backlog = 2048 # ----------------------------------------------------------------------------- diff --git a/labhub/templates/index.html b/labhub/templates/index.html index a26a976..9d23568 100644 --- a/labhub/templates/index.html +++ b/labhub/templates/index.html @@ -1 +1 @@ -
Pipeline
+{m.description}
++ Configure — {selectedMode.label} +
+ + {/* File picker */} +{inputFiles.length} file{inputFiles.length > 1 ? 's' : ''} selected. Files will be concatenated in the order listed.
+ )} ++ Factor of {dsFactor || '?'} → {numChannels ? Math.round(30000 / parseInt(dsFactor || 1)) + ' Hz output' : '—'} (assuming 30 kHz input) +
+{response.error}
+ )}
+ {response && response.ok && response.data && (
+ {response.data.operation} — submitted
+ +{response.data.output_folder}
+ Merge multiple .dat recordings into one file and downsample to LFP in a single pass.
+