Просмотр исходного кода

Migrate from Docker to native systemd + nginx deployment

Remove Docker containers (backend + nginx) in favor of running directly
on the host with systemd service management and system nginx. This
eliminates 9 volume mounts, privileged mode, and workarounds for
timezone, shutdown, WiFi, and self-update that Docker isolation required.

- Add dune-weaver.service (systemd unit with CPU pinning)
- Add nginx/dune-weaver.conf (site config, replaces nginx.conf)
- Rewrite setup-pi.sh: native-only flow with venv, Node.js, nginx, sudoers
- Rewrite dw CLI: remove all Docker branches, systemd-only commands
- Update backend: systemctl restart instead of docker restart
- Update frontend: "Restart Docker" → "Restart"
- Delete Dockerfile, frontend/Dockerfile, docker-compose.yml, nginx.conf,
  .github/workflows/docker-publish.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris 4 месяцев назад
Родитель
Сommit
0ed12bdf9c

+ 0 - 111
.github/workflows/docker-publish.yml

@@ -1,111 +0,0 @@
-name: Docker
-
-on:
-  push:
-    branches: [ "main" ]
-    paths:
-      - 'Dockerfile'
-      - 'frontend/Dockerfile'
-      - 'frontend/**'
-      - 'requirements.txt'
-      - 'VERSION'
-      - '**.py'
-  pull_request:
-    branches: [ "main" ]
-    paths:
-      - 'Dockerfile'
-      - 'frontend/Dockerfile'
-      - 'frontend/**'
-      - 'requirements.txt'
-      - 'VERSION'
-      - '**.py'
-  # Allow manual trigger on any branch
-  workflow_dispatch:
-    inputs:
-      branch:
-        description: 'Branch to build from'
-        required: false
-        default: ''
-
-env:
-  REGISTRY: ghcr.io
-
-jobs:
-  build-backend:
-    if: github.event_name != 'pull_request' || github.actor == 'tuanchris'
-    runs-on: ubuntu-latest
-    permissions:
-      contents: read
-      packages: write
-      id-token: write
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Log into registry ${{ env.REGISTRY }}
-        uses: docker/login-action@v3
-        with:
-          registry: ${{ env.REGISTRY }}
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Extract Docker metadata
-        id: meta
-        uses: docker/metadata-action@v5
-        with:
-          images: ${{ env.REGISTRY }}/${{ github.repository }}
-
-      - name: Build and push backend image
-        uses: docker/build-push-action@v5
-        with:
-          context: .
-          push: ${{ github.event_name != 'pull_request' }}
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
-          platforms: linux/amd64,linux/arm64
-          cache-from: type=gha,scope=backend
-          cache-to: type=gha,mode=max,scope=backend
-
-  build-frontend:
-    if: github.event_name != 'pull_request' || github.actor == 'tuanchris'
-    runs-on: ubuntu-latest
-    permissions:
-      contents: read
-      packages: write
-      id-token: write
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Log into registry ${{ env.REGISTRY }}
-        uses: docker/login-action@v3
-        with:
-          registry: ${{ env.REGISTRY }}
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Extract Docker metadata
-        id: meta
-        uses: docker/metadata-action@v5
-        with:
-          images: ${{ env.REGISTRY }}/${{ github.repository }}-frontend
-
-      - name: Build and push frontend image
-        uses: docker/build-push-action@v5
-        with:
-          context: ./frontend
-          file: ./frontend/Dockerfile
-          push: ${{ github.event_name != 'pull_request' }}
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
-          platforms: linux/amd64,linux/arm64
-          cache-from: type=gha,scope=frontend
-          cache-to: type=gha,mode=max,scope=frontend

+ 0 - 42
Dockerfile

@@ -1,42 +0,0 @@
-# Backend-only Dockerfile
-FROM python:3.11-slim-bookworm
-
-# Faster, repeatable builds
-ENV PYTHONDONTWRITEBYTECODE=1 \
-    PYTHONUNBUFFERED=1 \
-    PIP_NO_CACHE_DIR=1 \
-    PIP_DISABLE_PIP_VERSION_CHECK=1
-
-WORKDIR /app
-
-COPY requirements.txt ./
-RUN apt-get update && apt-get install -y --no-install-recommends \
-        gcc g++ make libjpeg-dev zlib1g-dev git \
-        # GPIO/NeoPixel support for DW LEDs
-        python3-dev python3-pip \
-        libgpiod2 libgpiod-dev \
-        scons \
-        systemd \
-        # nmcli for WiFi management (talks to host NetworkManager via mounted dbus socket)
-        network-manager \
-        # Docker CLI for container self-restart/update
-        ca-certificates curl gnupg \
-    && pip install --upgrade pip \
-    && pip install --no-cache-dir -r requirements.txt \
-    # Install Docker CLI from official Docker repo
-    && install -m 0755 -d /etc/apt/keyrings \
-    && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
-    && chmod a+r /etc/apt/keyrings/docker.gpg \
-    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \
-    && apt-get update \
-    && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \
-    && apt-get purge -y gcc g++ make scons \
-    && rm -rf /var/lib/apt/lists/*
-
-# Copy backend code
-COPY . .
-
-# Expose backend API port
-EXPOSE 8080
-
-CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

+ 0 - 47
docker-compose.yml

@@ -1,47 +0,0 @@
-services:
-  frontend:
-    build:
-      context: ./frontend
-      dockerfile: Dockerfile
-    image: ghcr.io/tuanchris/dune-weaver-frontend:${IMAGE_TAG:-main}
-    restart: always
-    ports:
-      - "${FRONTEND_PORT:-80}:80"
-    volumes:
-      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
-    depends_on:
-      - backend
-    container_name: dune-weaver-frontend
-
-  backend:
-    build: .
-    image: ghcr.io/tuanchris/dune-weaver:${IMAGE_TAG:-main}
-    restart: always
-    # Pin motion-critical backend to cores 0-2 (Raspberry Pi 4/5 has cores 0-3)
-    # This prevents CPU contention from touch app blocking I/O calls
-    cpuset: "0,1,2"
-    ports:
-      - "8080:8080"
-    # Environment variables for testing (uncomment to enable):
-    # environment:
-    #   FORCE_UPDATE_AVAILABLE: "1"        # Always show update available
-    #   FAKE_LATEST_VERSION: "99.0.0"      # Fake a newer version
-    volumes:
-      # Mount entire app directory for persistence
-      - .:/app
-      # Mount Docker socket for container self-restart/update
-      - /var/run/docker.sock:/var/run/docker.sock
-      # Mount timezone file from host for scheduling features
-      - /etc/timezone:/etc/host-timezone:ro
-      # Mount hostname for WiFi module to report host identity
-      - /etc/hostname:/etc/host-hostname:ro
-      # Mount systemd for host shutdown capability
-      - /run/systemd/system:/run/systemd/system:ro
-      - /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket:ro
-      - /sys/fs/cgroup:/sys/fs/cgroup:ro
-      # Mount GPIO for DW LEDs and Desert Compass (reed switch)
-      - /sys:/sys
-      # Mount /dev for serial port access (devices may not exist at start time)
-      - /dev:/dev
-    privileged: true
-    container_name: dune-weaver-backend

+ 1 - 1
dune-weaver-touch/dune-weaver-touch.service

@@ -17,7 +17,7 @@ Environment=QT_QPA_EGLFS_HIDECURSOR=1
 Environment=QT_QPA_EGLFS_INTEGRATION=eglfs_kms
 Environment=QT_QPA_EGLFS_INTEGRATION=eglfs_kms
 Environment=QT_QPA_EGLFS_KMS_ATOMIC=1
 Environment=QT_QPA_EGLFS_KMS_ATOMIC=1
 # CPU isolation: Pin touch app to core 3, lower priority to prevent starving motion backend
 # CPU isolation: Pin touch app to core 3, lower priority to prevent starving motion backend
-# Backend runs in Docker pinned to cores 0-2 for serial I/O timing reliability
+# Backend runs pinned to cores 0-2 for serial I/O timing reliability
 Nice=10
 Nice=10
 CPUQuota=25%
 CPUQuota=25%
 ExecStart=/usr/bin/taskset -c 3 /home/pi/dune-weaver-touch/venv/bin/python /home/pi/dune-weaver-touch/main.py
 ExecStart=/usr/bin/taskset -c 3 /home/pi/dune-weaver-touch/venv/bin/python /home/pi/dune-weaver-touch/main.py

+ 17 - 0
dune-weaver.service

@@ -0,0 +1,17 @@
+[Unit]
+Description=Dune Weaver Sand Table Controller
+After=network.target
+
+[Service]
+Type=simple
+User=USER_PLACEHOLDER
+WorkingDirectory=INSTALL_DIR_PLACEHOLDER
+ExecStart=INSTALL_DIR_PLACEHOLDER/.venv/bin/python INSTALL_DIR_PLACEHOLDER/main.py
+Restart=always
+RestartSec=5
+Environment=PYTHONUNBUFFERED=1
+CPUAffinity=0 1 2
+SupplementaryGroups=dialout gpio
+
+[Install]
+WantedBy=multi-user.target

+ 52 - 112
dw

@@ -5,15 +5,15 @@
 # Usage: dw <command>
 # Usage: dw <command>
 #
 #
 # Commands:
 # Commands:
-#   install     Run initial setup (Docker + WiFi fix)
+#   install     Run initial setup
 #   start       Start Dune Weaver
 #   start       Start Dune Weaver
 #   stop        Stop Dune Weaver
 #   stop        Stop Dune Weaver
 #   restart     Restart Dune Weaver
 #   restart     Restart Dune Weaver
 #   update      Pull latest changes and restart
 #   update      Pull latest changes and restart
 #   logs        View live logs (Ctrl+C to exit)
 #   logs        View live logs (Ctrl+C to exit)
-#   status      Show container status
-#   shell       Open a shell in the container
-#   checkout    Switch to a branch and pull its Docker images
+#   status      Show service status
+#   shell       Open a shell with the venv activated
+#   checkout    Switch to a branch and rebuild
 #   touch       Manage touch screen app
 #   touch       Manage touch screen app
 #   wifi        Manage WiFi and hotspot
 #   wifi        Manage WiFi and hotspot
 #   help        Show this help message
 #   help        Show this help message
@@ -42,19 +42,6 @@ find_install_dir() {
     fi
     fi
 }
 }
 
 
-# Check if using Docker or systemd
-is_docker_mode() {
-    # Docker mode if docker-compose.yml exists and docker is available
-    [[ -f "$INSTALL_DIR/docker-compose.yml" ]] && command -v docker &> /dev/null
-}
-
-# Set IMAGE_TAG from current git branch (e.g. feature/foo -> feature-foo)
-set_image_tag() {
-    local branch
-    branch=$(git -C "$INSTALL_DIR" rev-parse --abbrev-ref HEAD)
-    export IMAGE_TAG="${branch//\//-}"
-}
-
 INSTALL_DIR=$(find_install_dir)
 INSTALL_DIR=$(find_install_dir)
 
 
 # Check if installed
 # Check if installed
@@ -74,7 +61,7 @@ check_installed() {
 cmd_install() {
 cmd_install() {
     if [[ -z "$INSTALL_DIR" ]]; then
     if [[ -z "$INSTALL_DIR" ]]; then
         # Not installed, check if we're in the right directory
         # Not installed, check if we're in the right directory
-        if [[ -f "./docker-compose.yml" ]] && [[ -f "./main.py" ]]; then
+        if [[ -f "./main.py" ]] && [[ -f "./requirements.txt" ]]; then
             INSTALL_DIR=$(pwd)
             INSTALL_DIR=$(pwd)
         else
         else
             echo -e "${RED}Error: Run this from the dune-weaver directory${NC}"
             echo -e "${RED}Error: Run this from the dune-weaver directory${NC}"
@@ -93,47 +80,21 @@ cmd_install() {
 cmd_start() {
 cmd_start() {
     check_installed
     check_installed
     echo -e "${BLUE}Starting Dune Weaver...${NC}"
     echo -e "${BLUE}Starting Dune Weaver...${NC}"
-    cd "$INSTALL_DIR"
-
-    if is_docker_mode; then
-        set_image_tag
-        sudo -E docker compose up -d
-    else
-        sudo systemctl start dune-weaver
-    fi
-
-    local fe_port="${FRONTEND_PORT:-80}"
-    local port_suffix=""
-    [[ "$fe_port" != "80" ]] && port_suffix=":$fe_port"
-    echo -e "${GREEN}Started!${NC} Access at http://$(hostname -I | awk '{print $1}')${port_suffix}"
+    sudo systemctl start dune-weaver
+    echo -e "${GREEN}Started!${NC} Access at http://$(hostname -I | awk '{print $1}')"
 }
 }
 
 
 cmd_stop() {
 cmd_stop() {
     check_installed
     check_installed
     echo -e "${BLUE}Stopping Dune Weaver...${NC}"
     echo -e "${BLUE}Stopping Dune Weaver...${NC}"
-    cd "$INSTALL_DIR"
-
-    if is_docker_mode; then
-        sudo docker compose down
-    else
-        sudo systemctl stop dune-weaver
-    fi
-
+    sudo systemctl stop dune-weaver
     echo -e "${GREEN}Stopped${NC}"
     echo -e "${GREEN}Stopped${NC}"
 }
 }
 
 
 cmd_restart() {
 cmd_restart() {
     check_installed
     check_installed
     echo -e "${BLUE}Restarting Dune Weaver...${NC}"
     echo -e "${BLUE}Restarting Dune Weaver...${NC}"
-    cd "$INSTALL_DIR"
-
-    if is_docker_mode; then
-        set_image_tag
-        sudo -E docker compose restart
-    else
-        sudo systemctl restart dune-weaver
-    fi
-
+    sudo systemctl restart dune-weaver
     echo -e "${GREEN}Restarted${NC}"
     echo -e "${GREEN}Restarted${NC}"
 }
 }
 
 
@@ -157,30 +118,31 @@ cmd_update() {
         exec /usr/local/bin/dw update --continue
         exec /usr/local/bin/dw update --continue
     fi
     fi
 
 
-    if is_docker_mode; then
-        set_image_tag
-        echo -e "Image tag: ${BLUE}${IMAGE_TAG}${NC}"
-
-        echo "Pulling latest Docker image..."
-        sudo -E docker compose pull
+    echo "Updating Python dependencies..."
+    source .venv/bin/activate
+    pip install -r requirements.txt
 
 
-        echo "Stopping current container..."
-        sudo docker compose down
+    echo "Rebuilding frontend..."
+    cd "$INSTALL_DIR/frontend"
+    npm ci
+    npm run build
+    cd "$INSTALL_DIR"
 
 
-        echo "Starting with new version..."
-        sudo -E docker compose up -d --remove-orphans
+    # Update nginx config
+    echo "Updating nginx config..."
+    sudo cp "$INSTALL_DIR/nginx/dune-weaver.conf" /etc/nginx/sites-available/dune-weaver.conf
+    sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/nginx/sites-available/dune-weaver.conf
+    sudo nginx -t && sudo systemctl restart nginx
 
 
-        echo "Cleaning up unused Docker resources..."
-        sudo docker image prune -f
-        sudo docker container prune -f
-    else
-        echo "Updating Python dependencies..."
-        source .venv/bin/activate
-        pip install -r requirements.txt
+    # Update systemd service
+    echo "Updating systemd service..."
+    sudo cp "$INSTALL_DIR/dune-weaver.service" /etc/systemd/system/dune-weaver.service
+    sudo sed -i "s|USER_PLACEHOLDER|$USER|g" /etc/systemd/system/dune-weaver.service
+    sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/systemd/system/dune-weaver.service
+    sudo systemctl daemon-reload
 
 
-        echo "Restarting service..."
-        sudo systemctl restart dune-weaver
-    fi
+    echo "Restarting service..."
+    sudo systemctl restart dune-weaver
 
 
     # Install/update WiFi host scripts if not present
     # Install/update WiFi host scripts if not present
     if [[ -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]]; then
     if [[ -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]]; then
@@ -228,48 +190,27 @@ cmd_update() {
 
 
 cmd_logs() {
 cmd_logs() {
     check_installed
     check_installed
-    cd "$INSTALL_DIR"
 
 
     # Parse optional line count (e.g., dw logs 100)
     # Parse optional line count (e.g., dw logs 100)
     local lines="${1:-}"
     local lines="${1:-}"
-    local follow="-f"
 
 
     if [[ -n "$lines" ]]; then
     if [[ -n "$lines" ]]; then
-        follow="--tail $lines"
-    fi
-
-    if is_docker_mode; then
-        sudo docker compose logs $follow
+        sudo journalctl -u dune-weaver -n "$lines"
     else
     else
-        if [[ -n "$lines" ]]; then
-            sudo journalctl -u dune-weaver -n "$lines"
-        else
-            sudo journalctl -u dune-weaver -f
-        fi
+        sudo journalctl -u dune-weaver -f
     fi
     fi
 }
 }
 
 
 cmd_status() {
 cmd_status() {
     check_installed
     check_installed
-    cd "$INSTALL_DIR"
-
-    if is_docker_mode; then
-        sudo docker compose ps
-    else
-        sudo systemctl status dune-weaver
-    fi
+    sudo systemctl status dune-weaver
 }
 }
 
 
 cmd_shell() {
 cmd_shell() {
     check_installed
     check_installed
     cd "$INSTALL_DIR"
     cd "$INSTALL_DIR"
-
-    if is_docker_mode; then
-        sudo docker compose exec dune-weaver /bin/bash
-    else
-        echo -e "${YELLOW}Shell not available in systemd mode${NC}"
-        echo "Use: cd $INSTALL_DIR && source .venv/bin/activate"
-    fi
+    echo -e "${BLUE}Activating venv...${NC}"
+    exec bash --init-file <(echo "source '$INSTALL_DIR/.venv/bin/activate'; cd '$INSTALL_DIR'; echo 'Dune Weaver venv activated. Type \"exit\" to leave.'")
 }
 }
 
 
 # Touch app commands (systemd service)
 # Touch app commands (systemd service)
@@ -368,26 +309,25 @@ cmd_checkout() {
 
 
     echo -e "Switched to branch: ${GREEN}${branch}${NC}"
     echo -e "Switched to branch: ${GREEN}${branch}${NC}"
 
 
-    # Pull and restart Docker images for the new branch
-    if is_docker_mode; then
-        set_image_tag
-        echo -e "Image tag: ${BLUE}${IMAGE_TAG}${NC}"
-
-        echo "Pulling Docker images..."
-        sudo -E docker compose pull
+    # Rebuild frontend
+    echo "Rebuilding frontend..."
+    cd "$INSTALL_DIR/frontend"
+    npm ci
+    npm run build
+    cd "$INSTALL_DIR"
 
 
-        echo "Restarting with new images..."
-        sudo docker compose down
-        sudo -E docker compose up -d --remove-orphans
-
-        echo "Cleaning up old images..."
-        sudo docker image prune -f
-    fi
+    # Update Python dependencies
+    echo "Updating Python dependencies..."
+    source .venv/bin/activate
+    pip install -r requirements.txt
 
 
     # Update dw CLI from the new branch
     # Update dw CLI from the new branch
     sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
     sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
     sudo chmod +x /usr/local/bin/dw
     sudo chmod +x /usr/local/bin/dw
 
 
+    # Restart service
+    sudo systemctl restart dune-weaver
+
     # Install WiFi scripts if available on this branch but not yet installed
     # Install WiFi scripts if available on this branch but not yet installed
     if [[ -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]] && [[ ! -f /usr/local/bin/autohotspot ]]; then
     if [[ -f "$INSTALL_DIR/wifi/setup-wifi.sh" ]] && [[ ! -f /usr/local/bin/autohotspot ]]; then
         echo ""
         echo ""
@@ -507,15 +447,15 @@ cmd_help() {
     echo "Usage: dw <command>"
     echo "Usage: dw <command>"
     echo ""
     echo ""
     echo "Commands:"
     echo "Commands:"
-    echo "  install     Run initial setup (Docker + WiFi fix)"
+    echo "  install     Run initial setup"
     echo "  start       Start Dune Weaver"
     echo "  start       Start Dune Weaver"
     echo "  stop        Stop Dune Weaver"
     echo "  stop        Stop Dune Weaver"
     echo "  restart     Restart Dune Weaver"
     echo "  restart     Restart Dune Weaver"
     echo "  update      Pull latest changes and restart"
     echo "  update      Pull latest changes and restart"
-    echo "  checkout    Switch to a branch and pull its Docker images"
+    echo "  checkout    Switch to a branch and rebuild"
     echo "  logs [N]    View logs (N=number of lines, or follow if omitted)"
     echo "  logs [N]    View logs (N=number of lines, or follow if omitted)"
-    echo "  status      Show container status"
-    echo "  shell       Open a shell in the container"
+    echo "  status      Show service status"
+    echo "  shell       Open a shell with the venv activated"
     echo "  touch       Manage touch screen app (run 'dw touch help')"
     echo "  touch       Manage touch screen app (run 'dw touch help')"
     echo "  wifi        Manage WiFi and hotspot (run 'dw wifi help')"
     echo "  wifi        Manage WiFi and hotspot (run 'dw wifi help')"
     echo "  help        Show this help message"
     echo "  help        Show this help message"

+ 0 - 29
frontend/Dockerfile

@@ -1,29 +0,0 @@
-# Build stage
-FROM node:20-slim AS builder
-
-WORKDIR /app
-
-# Copy package files
-COPY package*.json ./
-
-# Install dependencies
-RUN npm ci
-
-# Copy source
-COPY . .
-
-# Override output to local directory for Docker build
-RUN npm run build -- --outDir ./dist
-
-# Production stage
-FROM nginx:alpine
-
-# Copy built files from builder
-COPY --from=builder /app/dist /usr/share/nginx/html
-
-# Copy nginx config (will be mounted or copied separately)
-# Note: nginx.conf should be copied in docker-compose or here
-
-EXPOSE 80
-
-CMD ["nginx", "-g", "daemon off;"]

+ 5 - 5
frontend/src/components/layout/Layout.tsx

@@ -595,13 +595,13 @@ export function Layout() {
   }
   }
 
 
   const handleRestart = async () => {
   const handleRestart = async () => {
-    if (!confirm('Are you sure you want to restart Docker containers?')) return
+    if (!confirm('Are you sure you want to restart Dune Weaver?')) return
 
 
     try {
     try {
       await apiClient.post('/api/system/restart')
       await apiClient.post('/api/system/restart')
-      toast.success('Docker containers are restarting...')
+      toast.success('Dune Weaver is restarting...')
     } catch {
     } catch {
-      toast.error('Failed to restart Docker containers')
+      toast.error('Failed to restart Dune Weaver')
     }
     }
   }
   }
 
 
@@ -1634,7 +1634,7 @@ export function Layout() {
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                   >
                   >
                     <span className="material-icons-outlined text-xl">restart_alt</span>
                     <span className="material-icons-outlined text-xl">restart_alt</span>
-                    Restart Docker
+                    Restart
                   </button>
                   </button>
                   <button
                   <button
                     onClick={handleShutdown}
                     onClick={handleShutdown}
@@ -1725,7 +1725,7 @@ export function Layout() {
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                   >
                   >
                     <span className="material-icons-outlined text-xl">restart_alt</span>
                     <span className="material-icons-outlined text-xl">restart_alt</span>
-                    Restart Docker
+                    Restart
                   </button>
                   </button>
                   <button
                   <button
                     onClick={() => {
                     onClick={() => {

+ 9 - 12
main.py

@@ -3946,7 +3946,7 @@ async def get_version_info(force_refresh: bool = False):
 
 
 @app.post("/api/update")
 @app.post("/api/update")
 async def trigger_update():
 async def trigger_update():
-    """Trigger software update by pulling latest Docker images and recreating containers."""
+    """Trigger software update by pulling latest code and restarting the service."""
     try:
     try:
         logger.info("Update triggered via API")
         logger.info("Update triggered via API")
         success, error_message, error_log = update_manager.update_software()
         success, error_message, error_log = update_manager.update_software()
@@ -3979,11 +3979,10 @@ async def shutdown_system():
         def delayed_shutdown():
         def delayed_shutdown():
             time.sleep(2)  # Give time for response to be sent
             time.sleep(2)  # Give time for response to be sent
             try:
             try:
-                # Use systemctl to shutdown the host (via mounted systemd socket)
-                subprocess.run(["systemctl", "poweroff"], check=True)
-                logger.info("Host shutdown command executed successfully via systemctl")
+                subprocess.run(["sudo", "systemctl", "poweroff"], check=True)
+                logger.info("System shutdown command executed successfully")
             except FileNotFoundError:
             except FileNotFoundError:
-                logger.error("systemctl command not found - ensure systemd volumes are mounted")
+                logger.error("systemctl command not found")
             except Exception as e:
             except Exception as e:
                 logger.error(f"Error executing host shutdown command: {e}")
                 logger.error(f"Error executing host shutdown command: {e}")
 
 
@@ -4001,7 +4000,7 @@ async def shutdown_system():
 
 
 @app.post("/api/system/restart")
 @app.post("/api/system/restart")
 async def restart_system():
 async def restart_system():
-    """Restart the Docker containers using docker compose"""
+    """Restart the Dune Weaver service via systemctl."""
     try:
     try:
         logger.warning("Restart initiated via API")
         logger.warning("Restart initiated via API")
 
 
@@ -4009,14 +4008,12 @@ async def restart_system():
         def delayed_restart():
         def delayed_restart():
             time.sleep(2)  # Give time for response to be sent
             time.sleep(2)  # Give time for response to be sent
             try:
             try:
-                # Use docker restart directly with container name
-                # This is simpler and doesn't require the compose file path
-                subprocess.run(["docker", "restart", "dune-weaver-backend"], check=True)
-                logger.info("Docker restart command executed successfully")
+                subprocess.run(["sudo", "systemctl", "restart", "dune-weaver"], check=True)
+                logger.info("Service restart command executed successfully")
             except FileNotFoundError:
             except FileNotFoundError:
-                logger.error("docker command not found")
+                logger.error("systemctl command not found")
             except Exception as e:
             except Exception as e:
-                logger.error(f"Error executing docker restart: {e}")
+                logger.error(f"Error executing service restart: {e}")
 
 
         import threading
         import threading
         restart_thread = threading.Thread(target=delayed_restart)
         restart_thread = threading.Thread(target=delayed_restart)

+ 2 - 7
modules/core/pattern_manager.py

@@ -237,15 +237,10 @@ def _get_timezone():
     else:
     else:
         # Fall back to system timezone detection
         # Fall back to system timezone detection
         try:
         try:
-            if os.path.exists('/etc/host-timezone'):
-                with open('/etc/host-timezone', 'r') as f:
-                    user_tz = f.read().strip()
-                    logger.info(f"Still Sands using timezone: {user_tz} (from host system)")
-            # Fallback to /etc/timezone if host-timezone doesn't exist
-            elif os.path.exists('/etc/timezone'):
+            if os.path.exists('/etc/timezone'):
                 with open('/etc/timezone', 'r') as f:
                 with open('/etc/timezone', 'r') as f:
                     user_tz = f.read().strip()
                     user_tz = f.read().strip()
-                    logger.info(f"Still Sands using timezone: {user_tz} (from container)")
+                    logger.info(f"Still Sands using timezone: {user_tz} (from system)")
             # Fallback to TZ environment variable
             # Fallback to TZ environment variable
             elif os.environ.get('TZ'):
             elif os.environ.get('TZ'):
                 user_tz = os.environ.get('TZ')
                 user_tz = os.environ.get('TZ')

+ 15 - 36
modules/update/update_manager.py

@@ -52,13 +52,10 @@ def check_git_updates():
 def update_software():
 def update_software():
     """Update the software to the latest version.
     """Update the software to the latest version.
 
 
-    This runs inside the Docker container, so it:
-    1. Pulls latest code via git (mounted volume at /app)
-    2. Pulls new Docker image for the backend
-    3. Restarts the container to apply updates
+    Pulls latest code, installs updated Python dependencies,
+    and restarts the systemd service.
 
 
-    Note: For a complete update including container recreation,
-    run 'dw update' from the host machine instead.
+    For a full update (including frontend rebuild), run 'dw update' instead.
     """
     """
     error_log = []
     error_log = []
     logger.info("Starting software update process")
     logger.info("Starting software update process")
@@ -73,53 +70,35 @@ def update_software():
             error_log.append(error_message)
             error_log.append(error_message)
             return None
             return None
 
 
-    # Step 1: Pull latest code via git (works because /app is mounted from host)
+    # Step 1: Pull latest code via git
     logger.info("Pulling latest code from git...")
     logger.info("Pulling latest code from git...")
     git_result = run_command(
     git_result = run_command(
         ["git", "pull", "--ff-only"],
         ["git", "pull", "--ff-only"],
-        "Failed to pull latest code from git",
-        cwd="/app"
+        "Failed to pull latest code from git"
     )
     )
     if git_result:
     if git_result:
         logger.info("Git pull completed successfully")
         logger.info("Git pull completed successfully")
 
 
-    # Step 2: Pull new Docker image for the backend only
-    # Note: There is no separate frontend image - it's either bundled or built locally
-    logger.info("Pulling latest Docker image...")
+    # Step 2: Install updated Python dependencies
+    logger.info("Installing updated dependencies...")
     run_command(
     run_command(
-        ["docker", "pull", "ghcr.io/tuanchris/dune-weaver:main"],
-        "Failed to pull backend Docker image"
+        [".venv/bin/pip", "install", "-r", "requirements.txt"],
+        "Failed to install updated dependencies"
     )
     )
 
 
-    # Step 3: Restart the backend container to apply updates
-    # We can't recreate ourselves from inside the container, so we just restart
-    # For full container recreation with new images, use 'dw update' from host
-    logger.info("Restarting backend container...")
-
-    # Use docker restart which works from inside the container
+    # Step 3: Restart the service
+    logger.info("Restarting dune-weaver service...")
     restart_result = run_command(
     restart_result = run_command(
-        ["docker", "restart", "dune-weaver-backend"],
-        "Failed to restart backend container"
+        ["sudo", "systemctl", "restart", "dune-weaver"],
+        "Failed to restart dune-weaver service"
     )
     )
 
 
     if not restart_result:
     if not restart_result:
-        # If docker restart fails, try a graceful approach
-        logger.info("Attempting graceful restart via compose...")
-        try:
-            # Just restart, don't try to recreate (which would fail)
-            subprocess.run(
-                ["docker", "compose", "restart", "backend"],
-                check=True,
-                cwd="/app"
-            )
-            logger.info("Container restarted successfully via compose")
-        except (subprocess.CalledProcessError, FileNotFoundError) as e:
-            logger.warning(f"Compose restart also failed: {e}")
-            error_log.append("Container restart failed - please run 'dw update' from host")
+        error_log.append("Service restart failed - please run 'dw restart' manually")
 
 
     if error_log:
     if error_log:
         logger.error(f"Software update completed with errors: {error_log}")
         logger.error(f"Software update completed with errors: {error_log}")
-        return False, "Update completed with errors. For best results, run 'dw update' from the host machine.", error_log
+        return False, "Update completed with errors. Run 'dw update' for a full update.", error_log
 
 
     logger.info("Software update completed successfully")
     logger.info("Software update completed successfully")
     return True, None, None
     return True, None, None

+ 3 - 28
modules/wifi/manager.py

@@ -2,8 +2,6 @@
 WiFi management via NetworkManager (nmcli).
 WiFi management via NetworkManager (nmcli).
 
 
 Handles scanning, connecting, and managing WiFi connections.
 Handles scanning, connecting, and managing WiFi connections.
-In Docker, nmcli communicates with the host's NetworkManager over the
-mounted D-Bus system socket — same mechanism systemctl uses for shutdown.
 """
 """
 
 
 import subprocess
 import subprocess
@@ -13,22 +11,11 @@ import asyncio
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-HOST_HOSTNAME_FILE = "/etc/host-hostname"
 HOTSPOT_CON_NAME = "DuneWeaver-Hotspot"
 HOTSPOT_CON_NAME = "DuneWeaver-Hotspot"
 
 
 
 
-def is_docker() -> bool:
-    """Check if running inside a Docker container."""
-    return os.path.exists("/.dockerenv") or os.getenv("DOCKER_CONTAINER") == "1"
-
-
 def run_nmcli(*args: str, timeout: int = 30) -> str:
 def run_nmcli(*args: str, timeout: int = 30) -> str:
-    """Run nmcli and return stdout.
-
-    In both Docker and venv modes, nmcli runs directly. In Docker, it
-    communicates with the host's NetworkManager via the mounted D-Bus
-    system bus socket (/var/run/dbus/system_bus_socket).
-    """
+    """Run nmcli and return stdout."""
     cmd = ["nmcli"] + list(args)
     cmd = ["nmcli"] + list(args)
     logger.debug(f"Running nmcli: {' '.join(cmd)}")
     logger.debug(f"Running nmcli: {' '.join(cmd)}")
     result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
     result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
@@ -109,18 +96,8 @@ def get_current_ip() -> str:
 
 
 
 
 def get_hostname() -> str:
 def get_hostname() -> str:
-    """Get the host system hostname.
-
-    In Docker, reads from the mounted /etc/host-hostname file to get the
-    real host identity instead of the container ID.
-    """
+    """Get the system hostname via NetworkManager."""
     try:
     try:
-        if is_docker() and os.path.exists(HOST_HOSTNAME_FILE):
-            with open(HOST_HOSTNAME_FILE, "r") as f:
-                name = f.read().strip()
-                if name:
-                    return name
-        # Fallback: use nmcli to query NM's hostname
         output = run_nmcli("general", "hostname")
         output = run_nmcli("general", "hostname")
         name = output.strip()
         name = output.strip()
         if name:
         if name:
@@ -451,8 +428,7 @@ def _trigger_autohotspot():
     """Trigger an immediate autohotspot check.
     """Trigger an immediate autohotspot check.
 
 
     Called when the active network is forgotten so the user doesn't have
     Called when the active network is forgotten so the user doesn't have
-    to wait for the next 60s timer tick. Falls back to direct nmcli
-    commands in Docker where the host script isn't available.
+    to wait for the next 60s timer tick.
     """
     """
     autohotspot_path = "/usr/local/bin/autohotspot"
     autohotspot_path = "/usr/local/bin/autohotspot"
     try:
     try:
@@ -464,7 +440,6 @@ def _trigger_autohotspot():
                 stderr=subprocess.DEVNULL,
                 stderr=subprocess.DEVNULL,
             )
             )
         else:
         else:
-            # Docker fallback: directly activate hotspot via D-Bus
             logger.info("Autohotspot script not found, activating hotspot directly...")
             logger.info("Autohotspot script not found, activating hotspot directly...")
             run_nmcli("dev", "disconnect", "wlan0")
             run_nmcli("dev", "disconnect", "wlan0")
             run_nmcli("con", "up", HOTSPOT_CON_NAME)
             run_nmcli("con", "up", HOTSPOT_CON_NAME)

+ 61 - 61
nginx.conf → nginx/dune-weaver.conf

@@ -1,61 +1,61 @@
-server {
-    listen 80;
-    server_name _;
-
-    # Increase max upload size for pattern files
-    client_max_body_size 10M;
-
-    # Frontend - serve static files
-    location / {
-        root /usr/share/nginx/html;
-        index index.html;
-        try_files $uri $uri/ /index.html;
-    }
-
-    # Captive portal detection probes → backend serves redirect page
-    # In hotspot mode, DNS resolves all domains to the Pi.
-    # Phones probe these well-known paths to detect captive portals.
-    location ~ ^/(hotspot-detect\.html|generate_204|connecttest\.txt|ncsi\.txt|redirect|canonical\.html)$ {
-        proxy_pass http://backend:8080;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-    }
-
-    # API proxy to backend container
-    location /api/ {
-        proxy_pass http://backend:8080;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-    }
-
-    # WebSocket proxy
-    location /ws/ {
-        proxy_pass http://backend:8080;
-        proxy_http_version 1.1;
-        proxy_set_header Upgrade $http_upgrade;
-        proxy_set_header Connection "upgrade";
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_read_timeout 86400;
-    }
-
-    # Static files from backend (pattern previews, custom logos)
-    location /static/ {
-        proxy_pass http://backend:8080;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_cache_valid 200 1h;
-    }
-
-    # All backend API endpoints (legacy non-/api/ routes)
-    location ~ ^/(list_theta_rho_files|preview_thr_batch|get_theta_rho_coordinates|upload_theta_rho|pause_execution|resume_execution|stop_execution|force_stop|soft_reset|skip_pattern|set_speed|get_speed|restart|shutdown|run_pattern|run_playlist|connect_device|disconnect_device|home_device|clear_sand|move_to_position|get_playlists|save_playlist|delete_playlist|rename_playlist|reorder_playlist|delete_theta_rho_file|get_status|list_serial_ports|serial_status|cache-progress|rebuild_cache|get_led_config|set_led_config|get_wled_ip|set_wled_ip|led|send_home|send_coordinate|move_to_center|move_to_perimeter|run_theta_rho|connect|disconnect|list_theta_rho_files_with_metadata|list_all_playlists|get_playlist|create_playlist|modify_playlist|add_to_playlist|preview_thr|preview|add_to_queue|restart_connection|controller_restart|recover_sensor_homing|run_theta_rho_file|download|check_software_update|update_software) {
-        proxy_pass http://backend:8080;
-        proxy_set_header Host $host;
-        proxy_set_header X-Real-IP $remote_addr;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto $scheme;
-    }
-}
+server {
+    listen 80;
+    server_name _;
+
+    # Increase max upload size for pattern files
+    client_max_body_size 10M;
+
+    # Frontend - serve static files
+    location / {
+        root INSTALL_DIR_PLACEHOLDER/static/dist;
+        index index.html;
+        try_files $uri $uri/ /index.html;
+    }
+
+    # Captive portal detection probes → backend serves redirect page
+    # In hotspot mode, DNS resolves all domains to the Pi.
+    # Phones probe these well-known paths to detect captive portals.
+    location ~ ^/(hotspot-detect\.html|generate_204|connecttest\.txt|ncsi\.txt|redirect|canonical\.html)$ {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    # API proxy to backend
+    location /api/ {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
+    # WebSocket proxy
+    location /ws/ {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_read_timeout 86400;
+    }
+
+    # Static files from backend (pattern previews, custom logos)
+    location /static/ {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_cache_valid 200 1h;
+    }
+
+    # All backend API endpoints (legacy non-/api/ routes)
+    location ~ ^/(list_theta_rho_files|preview_thr_batch|get_theta_rho_coordinates|upload_theta_rho|pause_execution|resume_execution|stop_execution|force_stop|soft_reset|skip_pattern|set_speed|get_speed|restart|shutdown|run_pattern|run_playlist|connect_device|disconnect_device|home_device|clear_sand|move_to_position|get_playlists|save_playlist|delete_playlist|rename_playlist|reorder_playlist|delete_theta_rho_file|get_status|list_serial_ports|serial_status|cache-progress|rebuild_cache|get_led_config|set_led_config|get_wled_ip|set_wled_ip|led|send_home|send_coordinate|move_to_center|move_to_perimeter|run_theta_rho|connect|disconnect|list_theta_rho_files_with_metadata|list_all_playlists|get_playlist|create_playlist|modify_playlist|add_to_playlist|preview_thr|preview|add_to_queue|restart_connection|controller_restart|recover_sensor_homing|run_theta_rho_file|download|check_software_update|update_software) {
+        proxy_pass http://127.0.0.1:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}

+ 84 - 107
setup-pi.sh

@@ -11,7 +11,6 @@
 #   bash setup-pi.sh
 #   bash setup-pi.sh
 #
 #
 # Options:
 # Options:
-#   --no-docker     Use Python venv instead of Docker
 #   --no-wifi-fix   Skip WiFi stability fix
 #   --no-wifi-fix   Skip WiFi stability fix
 #   --no-hotspot    Skip autohotspot setup
 #   --no-hotspot    Skip autohotspot setup
 #   --help          Show help
 #   --help          Show help
@@ -27,7 +26,6 @@ BLUE='\033[0;34m'
 NC='\033[0m' # No Color
 NC='\033[0m' # No Color
 
 
 # Default options
 # Default options
-USE_DOCKER=true
 FIX_WIFI=true  # Applied by default for stability
 FIX_WIFI=true  # Applied by default for stability
 SETUP_HOTSPOT=true  # Autohotspot for first-time WiFi setup
 SETUP_HOTSPOT=true  # Autohotspot for first-time WiFi setup
 INSTALL_DIR="$HOME/dune-weaver"
 INSTALL_DIR="$HOME/dune-weaver"
@@ -36,10 +34,6 @@ REPO_URL="https://github.com/tuanchris/dune-weaver"
 # Parse arguments
 # Parse arguments
 while [[ $# -gt 0 ]]; do
 while [[ $# -gt 0 ]]; do
     case $1 in
     case $1 in
-        --no-docker)
-            USE_DOCKER=false
-            shift
-            ;;
         --no-wifi-fix)
         --no-wifi-fix)
             FIX_WIFI=false
             FIX_WIFI=false
             shift
             shift
@@ -58,7 +52,6 @@ while [[ $# -gt 0 ]]; do
             echo "  cd ~/dune-weaver && bash setup-pi.sh [OPTIONS]"
             echo "  cd ~/dune-weaver && bash setup-pi.sh [OPTIONS]"
             echo ""
             echo ""
             echo "Options:"
             echo "Options:"
-            echo "  --no-docker     Use Python venv instead of Docker"
             echo "  --no-wifi-fix   Skip WiFi stability fix (applied by default)"
             echo "  --no-wifi-fix   Skip WiFi stability fix (applied by default)"
             echo "  --no-hotspot    Skip autohotspot setup"
             echo "  --no-hotspot    Skip autohotspot setup"
             echo "  --help, -h      Show this help message"
             echo "  --help, -h      Show this help message"
@@ -89,12 +82,40 @@ print_success() {
     echo -e "${GREEN}$1${NC}"
     echo -e "${GREEN}$1${NC}"
 }
 }
 
 
-# Install essential packages (git, vim)
-install_essentials() {
-    print_step "Installing essential packages..."
+# Install system dependencies
+install_system_deps() {
+    print_step "Installing system dependencies..."
     sudo apt update
     sudo apt update
-    sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" git vim
-    print_success "Essential packages installed"
+    sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \
+        python3-venv python3-pip python3-dev \
+        gcc g++ make \
+        libjpeg-dev zlib1g-dev \
+        libgpiod2 libgpiod-dev \
+        nginx git vim
+    print_success "System dependencies installed"
+}
+
+# Install Node.js 20 via nodesource
+install_nodejs() {
+    print_step "Installing Node.js..."
+
+    if command -v node &> /dev/null; then
+        local node_version
+        node_version=$(node --version)
+        echo "Node.js already installed: $node_version"
+        # Check if version is 20+
+        local major
+        major=$(echo "$node_version" | sed 's/v//' | cut -d. -f1)
+        if [[ "$major" -ge 20 ]]; then
+            print_success "Node.js version is sufficient"
+            return
+        fi
+        echo "Upgrading to Node.js 20..."
+    fi
+
+    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
+    sudo DEBIAN_FRONTEND=noninteractive apt install -y nodejs
+    print_success "Node.js $(node --version) installed"
 }
 }
 
 
 # Check if running on Raspberry Pi
 # Check if running on Raspberry Pi
@@ -177,44 +198,12 @@ update_system() {
     print_success "System updated"
     print_success "System updated"
 }
 }
 
 
-# Install Docker
-install_docker() {
-    print_step "Installing Docker..."
-
-    if command -v docker &> /dev/null; then
-        echo "Docker already installed: $(docker --version)"
-    else
-        curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
-        sudo sh /tmp/get-docker.sh
-        rm /tmp/get-docker.sh
-        print_success "Docker installed"
-    fi
-
-    # Add user to docker group
-    if ! groups $USER | grep -q docker; then
-        print_step "Adding $USER to docker group..."
-        sudo usermod -aG docker $USER
-        DOCKER_GROUP_ADDED=true
-        print_warning "You'll need to log out and back in for docker group changes to take effect"
-    fi
-}
-
-# Install Python dependencies (non-Docker)
-install_python_deps() {
-    print_step "Installing Python dependencies..."
-
-    # Install system packages
-    sudo apt install -y python3-venv python3-pip git
-
-    print_success "Python dependencies installed"
-}
-
 # Verify we're in the dune-weaver directory
 # Verify we're in the dune-weaver directory
 ensure_repo() {
 ensure_repo() {
     print_step "Setting up dune-weaver repository..."
     print_step "Setting up dune-weaver repository..."
 
 
     # Check if we're already in the dune-weaver directory
     # Check if we're already in the dune-weaver directory
-    if [[ -f "docker-compose.yml" ]] && [[ -f "main.py" ]]; then
+    if [[ -f "main.py" ]] && [[ -f "requirements.txt" ]]; then
         INSTALL_DIR="$(pwd)"
         INSTALL_DIR="$(pwd)"
         print_success "Using existing repo at $INSTALL_DIR"
         print_success "Using existing repo at $INSTALL_DIR"
         return
         return
@@ -236,29 +225,8 @@ ensure_repo() {
     print_success "Cloned to $INSTALL_DIR"
     print_success "Cloned to $INSTALL_DIR"
 }
 }
 
 
-# Install dw CLI command
-install_cli() {
-    print_step "Installing 'dw' command..."
-
-    # Copy dw script to /usr/local/bin
-    sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
-    sudo chmod +x /usr/local/bin/dw
-
-    print_success "'dw' command installed"
-}
-
-# Deploy with Docker
-deploy_docker() {
-    print_step "Deploying Dune Weaver with Docker Compose..."
-
-    cd "$INSTALL_DIR"
-    sudo docker compose up -d --quiet-pull
-
-    print_success "Docker deployment complete!"
-}
-
-# Deploy with Python venv
-deploy_python() {
+# Deploy native (venv + systemd + nginx)
+deploy_native() {
     print_step "Setting up Python virtual environment..."
     print_step "Setting up Python virtual environment..."
 
 
     cd "$INSTALL_DIR"
     cd "$INSTALL_DIR"
@@ -272,31 +240,57 @@ deploy_python() {
     pip install --upgrade pip
     pip install --upgrade pip
     pip install -r requirements.txt
     pip install -r requirements.txt
 
 
-    # Create systemd service
-    print_step "Creating systemd service..."
-
-    sudo tee /etc/systemd/system/dune-weaver.service > /dev/null << EOF
-[Unit]
-Description=Dune Weaver Backend
-After=network.target
+    # Build frontend
+    print_step "Building frontend..."
+    cd "$INSTALL_DIR/frontend"
+    npm ci
+    npm run build
+    cd "$INSTALL_DIR"
 
 
-[Service]
-ExecStart=$INSTALL_DIR/.venv/bin/python $INSTALL_DIR/main.py
-WorkingDirectory=$INSTALL_DIR
-Restart=always
-User=$USER
-Environment=PYTHONUNBUFFERED=1
+    # Configure nginx
+    print_step "Configuring nginx..."
+    sudo cp "$INSTALL_DIR/nginx/dune-weaver.conf" /etc/nginx/sites-available/dune-weaver.conf
+    sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/nginx/sites-available/dune-weaver.conf
+    sudo ln -sf /etc/nginx/sites-available/dune-weaver.conf /etc/nginx/sites-enabled/dune-weaver.conf
+    sudo rm -f /etc/nginx/sites-enabled/default
+    sudo nginx -t
+    sudo systemctl restart nginx
+    sudo systemctl enable nginx
 
 
-[Install]
-WantedBy=multi-user.target
-EOF
+    # Create systemd service
+    print_step "Creating systemd service..."
+    sudo cp "$INSTALL_DIR/dune-weaver.service" /etc/systemd/system/dune-weaver.service
+    sudo sed -i "s|USER_PLACEHOLDER|$USER|g" /etc/systemd/system/dune-weaver.service
+    sudo sed -i "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" /etc/systemd/system/dune-weaver.service
 
 
     # Enable and start service
     # Enable and start service
     sudo systemctl daemon-reload
     sudo systemctl daemon-reload
     sudo systemctl enable dune-weaver
     sudo systemctl enable dune-weaver
     sudo systemctl start dune-weaver
     sudo systemctl start dune-weaver
 
 
-    print_success "Python deployment complete!"
+    # Create sudoers entry for passwordless systemctl commands
+    print_step "Configuring sudo permissions..."
+    sudo tee /etc/sudoers.d/dune-weaver > /dev/null << EOF
+$USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart dune-weaver
+$USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop dune-weaver
+$USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl start dune-weaver
+$USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff
+$USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
+EOF
+    sudo chmod 0440 /etc/sudoers.d/dune-weaver
+
+    print_success "Native deployment complete!"
+}
+
+# Install dw CLI command
+install_cli() {
+    print_step "Installing 'dw' command..."
+
+    # Copy dw script to /usr/local/bin
+    sudo cp "$INSTALL_DIR/dw" /usr/local/bin/dw
+    sudo chmod +x /usr/local/bin/dw
+
+    print_success "'dw' command installed"
 }
 }
 
 
 # Setup autohotspot
 # Setup autohotspot
@@ -336,8 +330,8 @@ print_final_instructions() {
     echo -e "${GREEN}============================================${NC}"
     echo -e "${GREEN}============================================${NC}"
     echo ""
     echo ""
     echo -e "Access the web interface at:"
     echo -e "Access the web interface at:"
-    echo -e "  ${BLUE}http://$IP:8080${NC}"
-    echo -e "  ${BLUE}http://$HOSTNAME.local:8080${NC}"
+    echo -e "  ${BLUE}http://$IP${NC}"
+    echo -e "  ${BLUE}http://$HOSTNAME.local${NC}"
     echo ""
     echo ""
 
 
     echo "Manage with the 'dw' command:"
     echo "Manage with the 'dw' command:"
@@ -357,10 +351,6 @@ print_final_instructions() {
         echo ""
         echo ""
     fi
     fi
 
 
-    if [[ "$DOCKER_GROUP_ADDED" == "true" ]]; then
-        print_warning "Please log out and back in for docker group changes to take effect"
-    fi
-
     if [[ "$NEEDS_REBOOT" == "true" ]]; then
     if [[ "$NEEDS_REBOOT" == "true" ]]; then
         print_warning "A reboot is recommended to apply WiFi fixes"
         print_warning "A reboot is recommended to apply WiFi fixes"
         read -p "Reboot now? (y/N) " -n 1 -r
         read -p "Reboot now? (y/N) " -n 1 -r
@@ -382,19 +372,13 @@ main() {
     echo -e "${NC}"
     echo -e "${NC}"
     echo "Raspberry Pi Setup Script"
     echo "Raspberry Pi Setup Script"
     echo ""
     echo ""
-
-    # Detect deployment method
-    if [[ "$USE_DOCKER" == "true" ]]; then
-        echo "Deployment method: Docker (recommended)"
-    else
-        echo "Deployment method: Python virtual environment"
-    fi
     echo "Install directory: $INSTALL_DIR"
     echo "Install directory: $INSTALL_DIR"
     echo ""
     echo ""
 
 
     # Run setup steps
     # Run setup steps
     check_raspberry_pi
     check_raspberry_pi
-    install_essentials
+    install_system_deps
+    install_nodejs
     ensure_repo
     ensure_repo
     update_system
     update_system
     disable_wlan_powersave
     disable_wlan_powersave
@@ -407,14 +391,7 @@ main() {
         setup_autohotspot
         setup_autohotspot
     fi
     fi
 
 
-    if [[ "$USE_DOCKER" == "true" ]]; then
-        install_docker
-        deploy_docker
-    else
-        install_python_deps
-        deploy_python
-    fi
-
+    deploy_native
     install_cli
     install_cli
     print_final_instructions
     print_final_instructions
 }
 }

+ 1 - 1
wifi/autohotspot.service

@@ -1,7 +1,7 @@
 [Unit]
 [Unit]
 Description=Dune Weaver Autohotspot
 Description=Dune Weaver Autohotspot
 After=NetworkManager.service
 After=NetworkManager.service
-Before=docker.service
+Before=dune-weaver.service
 
 
 [Service]
 [Service]
 Type=oneshot
 Type=oneshot