Browse Source

Add Docker-based build and deployment packaging for Debian 13

Introduces a complete deployment pipeline: Docker multi-stage build with
ccache/npm caching, tar.gz package creation (microbit + database + webui),
install/update scripts with config preservation and rolling updates,
systemd service units with security hardening, and nginx reverse proxy
setup with Let's Encrypt SSL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fszontagh 2 months ago
parent
commit
57ded29e90

+ 18 - 0
.dockerignore

@@ -0,0 +1,18 @@
+build/
+cmake-build-*/
+webui/node_modules/
+webui/dist/
+webui/.vite/
+.git/
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+*.key
+.env*
+config/local.json
+dist/
+compile_commands.json
+.DS_Store
+Thumbs.db

+ 54 - 0
packaging/Dockerfile.base

@@ -0,0 +1,54 @@
+# Smartbotic Build Base Image
+# Contains all build dependencies pre-installed for fast builds
+#
+# Build:
+#   docker buildx build -f packaging/Dockerfile.base -t smartbotic-build-base:debian13 .
+
+FROM debian:trixie
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Install all build dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    # Build tools
+    build-essential \
+    cmake \
+    ninja-build \
+    git \
+    pkg-config \
+    ca-certificates \
+    # Compiler cache for faster rebuilds
+    ccache \
+    # Networking
+    libcurl4-openssl-dev \
+    libssl-dev \
+    # JSON & Logging
+    nlohmann-json3-dev \
+    libspdlog-dev \
+    # Systemd (for sd_notify)
+    libsystemd-dev \
+    # gRPC/Protobuf
+    protobuf-compiler \
+    libprotobuf-dev \
+    libgrpc++-dev \
+    protobuf-compiler-grpc \
+    # Compression for database storage
+    liblz4-dev \
+    # WebUI build
+    nodejs \
+    npm \
+    && rm -rf /var/lib/apt/lists/*
+
+# Pre-install latest npm
+RUN npm install -g npm@latest
+
+# Configure ccache
+ENV CCACHE_DIR=/ccache
+ENV CCACHE_MAXSIZE=5G
+ENV CCACHE_COMPRESS=1
+ENV CCACHE_COMPRESSLEVEL=6
+
+WORKDIR /build
+
+# Add a marker file to identify base image version
+RUN echo "smartbotic-build-base:debian13:v1" > /etc/smartbotic-build-base

+ 103 - 0
packaging/Dockerfile.build

@@ -0,0 +1,103 @@
+# syntax=docker/dockerfile:1.4
+# Smartbotic Package Builder
+# Debian 13 (trixie) based build container for production binaries
+#
+# Usage (standard build - installs dependencies each time):
+#   docker buildx build -f packaging/Dockerfile.build --target packages --output "type=local,dest=dist/" .
+#
+# Usage (fast build with pre-built base image):
+#   # First build the base image once:
+#   docker buildx build -f packaging/Dockerfile.base -t smartbotic-build-base:debian13 .
+#   # Then use it for fast builds:
+#   docker buildx build -f packaging/Dockerfile.build \
+#     --build-arg BASE_IMAGE=smartbotic-build-base:debian13 \
+#     --build-arg BUILD_VERSION=x.y.z \
+#     --build-arg BUILD_GIT_COMMIT=$(git rev-parse HEAD) \
+#     --build-arg BUILD_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \
+#     --target packages --output "type=local,dest=dist/" .
+#
+# Build caches are persistent across builds via BuildKit cache mounts:
+#   - /ccache: C++ compiler cache (ccache)
+#   - /npm-cache: npm package cache
+
+ARG BASE_IMAGE=debian:trixie
+FROM ${BASE_IMAGE} AS builder
+
+ARG BUILD_VERSION=0.0.0
+ARG BUILD_TARGET=debian13
+ARG BUILD_JOBS=16
+ARG BUILD_GIT_COMMIT=unknown
+ARG BUILD_GIT_BRANCH=unknown
+ENV DEBIAN_FRONTEND=noninteractive
+ENV BUILD_VERSION=${BUILD_VERSION}
+ENV BUILD_TARGET=${BUILD_TARGET}
+ENV BUILD_JOBS=${BUILD_JOBS}
+ENV BUILD_GIT_COMMIT=${BUILD_GIT_COMMIT}
+ENV BUILD_GIT_BRANCH=${BUILD_GIT_BRANCH}
+
+# ccache configuration
+ENV CCACHE_DIR=/ccache
+ENV CCACHE_MAXSIZE=5G
+ENV CCACHE_COMPRESS=1
+ENV CCACHE_COMPRESSLEVEL=6
+ENV PATH="/usr/lib/ccache:${PATH}"
+
+# Install build dependencies (skipped if using pre-built base image)
+RUN if [ ! -f /etc/smartbotic-build-base ]; then \
+    apt-get update && apt-get install -y --no-install-recommends \
+    build-essential cmake ninja-build git pkg-config ca-certificates ccache \
+    libcurl4-openssl-dev libssl-dev \
+    nlohmann-json3-dev libspdlog-dev libsystemd-dev \
+    protobuf-compiler libprotobuf-dev libgrpc++-dev protobuf-compiler-grpc \
+    liblz4-dev nodejs npm \
+    && rm -rf /var/lib/apt/lists/*; \
+    else echo "Using pre-built base image: $(cat /etc/smartbotic-build-base)"; fi
+
+WORKDIR /build
+
+# Copy package files first for better layer caching
+COPY webui/package.json webui/package-lock.json ./webui/
+
+# Install npm dependencies with cache mount
+RUN --mount=type=cache,target=/npm-cache,id=npm-cache \
+    cd webui && npm ci --cache /npm-cache
+
+# Copy WebUI source and build
+COPY webui/ ./webui/
+RUN cd webui && npm run build
+
+# Copy C++ source code (excluding webui which is already copied)
+COPY CMakeLists.txt ./
+COPY cmake/ ./cmake/
+COPY lib/ ./lib/
+COPY src/ ./src/
+COPY config/ ./config/
+COPY packaging/ ./packaging/
+COPY external/ ./external/
+
+# Build all targets with ccache and Release optimizations
+RUN --mount=type=cache,target=/ccache,id=ccache \
+    cmake -B build -G Ninja \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DCMAKE_C_COMPILER_LAUNCHER=ccache \
+    -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
+    -DBUILD_TESTS=OFF \
+    -DBUNDLE_DATABASE=ON \
+    -DGIT_COMMIT_OVERRIDE=${BUILD_GIT_COMMIT} \
+    -DGIT_BRANCH_OVERRIDE=${BUILD_GIT_BRANCH} \
+    && cmake --build build --parallel ${BUILD_JOBS} \
+    && ccache --show-stats || true
+
+# Package creation stage
+FROM builder AS packager
+
+# Create package output directory
+RUN mkdir -p /packages
+
+# Run package creation script
+RUN chmod +x /build/packaging/scripts/create-packages.sh && \
+    /build/packaging/scripts/create-packages.sh
+
+# Output stage - just the packages
+FROM scratch AS packages
+COPY --from=packager /packages/*.tar.gz /

+ 258 - 0
packaging/build.sh

@@ -0,0 +1,258 @@
+#!/bin/bash
+#
+# Smartbotic Production Build Script
+#
+# This script handles the Docker build workflow:
+# 1. Checks if the pre-built base image exists
+# 2. Creates it if needed
+# 3. Builds production packages using the base image
+#
+# Usage:
+#   ./packaging/build.sh              # Build with auto-detected version
+#   ./packaging/build.sh 0.1.0        # Build with specific version
+#   ./packaging/build.sh --rebuild-base  # Force rebuild the base image
+#
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+cd "$PROJECT_DIR"
+
+# Configuration
+BASE_IMAGE_NAME="smartbotic-build-base:debian13"
+OUTPUT_DIR="${PROJECT_DIR}/dist"
+BUILD_JOBS="${BUILD_JOBS:-16}"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+log_info() {
+    echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+    echo -e "${GREEN}[OK]${NC} $1"
+}
+
+log_warn() {
+    echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+    echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Parse arguments
+REBUILD_BASE=false
+CLEAR_CACHE=false
+NO_CACHE=false
+VERSION=""
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --rebuild-base)
+            REBUILD_BASE=true
+            shift
+            ;;
+        --clear-cache)
+            CLEAR_CACHE=true
+            shift
+            ;;
+        --no-cache)
+            NO_CACHE=true
+            shift
+            ;;
+        --help|-h)
+            echo "Smartbotic Production Build Script"
+            echo ""
+            echo "Usage: $0 [OPTIONS] [VERSION]"
+            echo ""
+            echo "Options:"
+            echo "  --rebuild-base    Force rebuild the base image"
+            echo "  --clear-cache     Clear BuildKit build caches (ccache, npm)"
+            echo "  --no-cache        Build without using caches"
+            echo "  --help, -h        Show this help message"
+            echo ""
+            echo "Arguments:"
+            echo "  VERSION           Version string (default: auto-detect from CMakeLists.txt)"
+            echo ""
+            echo "Environment variables:"
+            echo "  BUILD_JOBS        Number of parallel build jobs (default: 16)"
+            echo ""
+            echo "Examples:"
+            echo "  $0                # Build with auto-detected version"
+            echo "  $0 0.1.0          # Build version 0.1.0"
+            echo "  $0 --rebuild-base # Rebuild base image and build packages"
+            echo "  $0 --clear-cache  # Clear caches and build fresh"
+            exit 0
+            ;;
+        *)
+            VERSION="$1"
+            shift
+            ;;
+    esac
+done
+
+# Auto-detect version if not specified
+if [ -z "$VERSION" ]; then
+    VERSION=$(grep -oP 'project\(smartbotic-microbit VERSION \K[0-9.]+' CMakeLists.txt)
+    if [ -z "$VERSION" ]; then
+        log_error "Could not detect version from CMakeLists.txt"
+        exit 1
+    fi
+fi
+
+log_info "Smartbotic Build Script"
+log_info "Version: $VERSION"
+log_info "Output directory: $OUTPUT_DIR"
+echo ""
+
+# Check if Docker is available
+if ! command -v docker &> /dev/null; then
+    log_error "Docker is not installed or not in PATH"
+    exit 1
+fi
+
+# Check if base image exists
+base_image_exists() {
+    docker image inspect "$BASE_IMAGE_NAME" &> /dev/null
+}
+
+# Build base image
+build_base_image() {
+    log_info "Building base image: $BASE_IMAGE_NAME"
+    log_info "This may take several minutes on first run..."
+    echo ""
+
+    docker buildx build \
+        -f packaging/Dockerfile.base \
+        -t "$BASE_IMAGE_NAME" \
+        .
+
+    log_success "Base image built successfully"
+    echo ""
+}
+
+# Build production packages
+build_packages() {
+    log_info "Building production packages..."
+    log_info "Using base image: $BASE_IMAGE_NAME"
+    log_info "Build jobs: $BUILD_JOBS"
+    if [ "$NO_CACHE" = true ]; then
+        log_warn "Building without caches (--no-cache)"
+    else
+        log_info "Using ccache and npm cache for faster builds"
+    fi
+    echo ""
+
+    # Get git info
+    GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
+    GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
+
+    log_info "Git commit: $GIT_COMMIT"
+    log_info "Git branch: $GIT_BRANCH"
+    echo ""
+
+    # Clean output directory
+    rm -rf "$OUTPUT_DIR"
+    mkdir -p "$OUTPUT_DIR"
+
+    # Build arguments
+    BUILD_ARGS=(
+        -f packaging/Dockerfile.build
+        --build-arg BASE_IMAGE="$BASE_IMAGE_NAME"
+        --build-arg BUILD_VERSION="$VERSION"
+        --build-arg BUILD_GIT_COMMIT="$GIT_COMMIT"
+        --build-arg BUILD_GIT_BRANCH="$GIT_BRANCH"
+        --build-arg BUILD_JOBS="$BUILD_JOBS"
+        --target packages
+        --output "type=local,dest=$OUTPUT_DIR"
+    )
+
+    # Add --no-cache if requested
+    if [ "$NO_CACHE" = true ]; then
+        BUILD_ARGS+=(--no-cache)
+    fi
+
+    # Build packages
+    docker buildx build "${BUILD_ARGS[@]}" .
+
+    log_success "Packages built successfully"
+    echo ""
+}
+
+# Clear build caches
+clear_build_cache() {
+    log_info "Clearing BuildKit build caches..."
+    docker builder prune --filter type=exec.cachemount -f
+    log_success "Build caches cleared"
+    echo ""
+}
+
+# Main flow
+echo "=================================================="
+echo "  Smartbotic Production Build"
+echo "  Version: $VERSION"
+echo "=================================================="
+echo ""
+
+# Clear cache if requested
+if [ "$CLEAR_CACHE" = true ]; then
+    clear_build_cache
+fi
+
+# Check/build base image
+if [ "$REBUILD_BASE" = true ]; then
+    log_warn "Force rebuilding base image..."
+    build_base_image
+elif base_image_exists; then
+    log_success "Base image found: $BASE_IMAGE_NAME"
+else
+    log_warn "Base image not found, building..."
+    build_base_image
+fi
+
+# Build packages
+build_packages
+
+# List created packages
+echo "=================================================="
+echo "  Build Complete"
+echo "=================================================="
+echo ""
+log_info "Created packages in $OUTPUT_DIR:"
+echo ""
+ls -lh "$OUTPUT_DIR"/*.tar.gz 2>/dev/null | while read line; do
+    echo "  $line"
+done
+echo ""
+
+# Calculate total size
+TOTAL_SIZE=$(du -sh "$OUTPUT_DIR" | cut -f1)
+log_info "Total size: $TOTAL_SIZE"
+echo ""
+
+# Show cache info
+echo "=================================================="
+echo "  Build Cache Info"
+echo "=================================================="
+echo ""
+log_info "BuildKit caches (ccache, npm) are stored persistently."
+log_info "Subsequent builds will be faster due to compilation caching."
+echo ""
+log_info "Cache management commands:"
+echo "  $0 --clear-cache   # Clear all build caches"
+echo "  $0 --no-cache      # Build without using caches"
+echo ""
+
+log_success "Build completed successfully!"
+echo ""
+echo "To deploy to production:"
+echo "  scp dist/*.tar.gz root@<server>:/tmp/smartbotic-update/"
+echo "  ssh root@<server> '/opt/smartbotic/bin/smartbotic-update --rolling --backup /tmp/smartbotic-update/'"
+echo ""

+ 12 - 0
packaging/dependencies.conf

@@ -0,0 +1,12 @@
+# Smartbotic Runtime Dependencies
+# Format: service=package1 package2 package3
+# These are the runtime dependencies for Debian 13 (trixie)
+
+# Common dependencies (required by all services)
+common=libcurl4t64 libssl3t64 libprotobuf32t64 libgrpc++1.51t64 libspdlog1.15 libsystemd0
+
+# Database service
+database=liblz4-1
+
+# MicroBit service (all deps covered by common)
+microbit=

+ 124 - 0
packaging/nginx/smartbotic.conf.template

@@ -0,0 +1,124 @@
+# Smartbotic Nginx Configuration
+# This file is managed by Smartbotic installer
+# Domain: {{DOMAIN}}
+
+# Rate limiting
+limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
+limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
+
+# Upstream definition
+upstream smartbotic_http {
+    server 127.0.0.1:8090;
+    keepalive 32;
+}
+
+# HTTP -> HTTPS redirect
+server {
+    listen 80;
+    listen [::]:80;
+    server_name {{SERVER_NAMES}};
+
+    # Let's Encrypt challenge
+    location /.well-known/acme-challenge/ {
+        root /var/www/certbot;
+    }
+
+    # Redirect all other traffic to HTTPS
+    location / {
+        return 301 https://$host$request_uri;
+    }
+}
+
+# HTTPS server
+server {
+    listen 443 ssl;
+    listen [::]:443 ssl;
+    http2 on;
+    server_name {{SERVER_NAMES}};
+
+    # SSL certificates (managed by certbot)
+    ssl_certificate /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem;
+    ssl_trusted_certificate /etc/letsencrypt/live/{{DOMAIN}}/chain.pem;
+
+    # Modern SSL configuration
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+    ssl_prefer_server_ciphers off;
+    ssl_session_timeout 1d;
+    ssl_session_cache shared:SSL:50m;
+    ssl_session_tickets off;
+
+    # OCSP stapling
+    ssl_stapling on;
+    ssl_stapling_verify on;
+    resolver 1.1.1.1 8.8.8.8 valid=300s;
+    resolver_timeout 5s;
+
+    # Security headers
+    add_header Strict-Transport-Security "max-age=63072000" always;
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header X-XSS-Protection "1; mode=block" always;
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+    # Logging
+    access_log /var/log/nginx/smartbotic_access.log;
+    error_log /var/log/nginx/smartbotic_error.log;
+
+    # Client settings
+    client_max_body_size 50M;
+    client_body_timeout 60s;
+    client_header_timeout 60s;
+
+    # Gzip compression
+    gzip on;
+    gzip_vary on;
+    gzip_proxied any;
+    gzip_comp_level 6;
+    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
+
+    # API endpoints
+    location /api/ {
+        limit_req zone=api_limit burst=50 nodelay;
+        limit_conn conn_limit 100;
+
+        proxy_pass http://smartbotic_http;
+        proxy_http_version 1.1;
+        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;
+        proxy_set_header Connection "";
+        proxy_read_timeout 300s;
+        proxy_send_timeout 300s;
+        proxy_connect_timeout 60s;
+        proxy_buffering off;
+    }
+
+    # WebUI static files and SPA fallback
+    location / {
+        proxy_pass http://smartbotic_http;
+        proxy_http_version 1.1;
+        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;
+        proxy_set_header Connection "";
+
+        # Cache static assets
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+            proxy_pass http://smartbotic_http;
+            proxy_cache_valid 200 1d;
+            add_header Cache-Control "public, max-age=86400";
+        }
+    }
+
+    # Health check endpoint (no rate limit)
+    location /health {
+        proxy_pass http://smartbotic_http/api/v1/health;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header Connection "";
+    }
+}

+ 251 - 0
packaging/scripts/create-packages.sh

@@ -0,0 +1,251 @@
+#!/bin/bash
+# Smartbotic Package Creation Script
+# Creates tar.gz distribution packages from built binaries
+#
+# Environment variables:
+#   BUILD_DIR    - Directory containing built binaries (default: /build/build)
+#   SOURCE_DIR   - Source directory (default: /build)
+#   OUTPUT_DIR   - Output directory for packages (default: /packages)
+#   BUILD_VERSION - Package version (default: 0.1.0)
+
+set -e
+
+BUILD_DIR="${BUILD_DIR:-/build/build}"
+SOURCE_DIR="${SOURCE_DIR:-/build}"
+OUTPUT_DIR="${OUTPUT_DIR:-/packages}"
+BUILD_VERSION="${BUILD_VERSION:-0.1.0}"
+BUILD_TARGET="${BUILD_TARGET:-debian13}"
+
+# Colors for output
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
+print_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
+
+# Load dependencies from config
+DEPS_FILE="$SOURCE_DIR/packaging/dependencies.conf"
+
+# Get dependencies for a service (includes common deps)
+get_deps() {
+    local service="$1"
+    local common_deps=""
+    local service_deps=""
+
+    if [ -f "$DEPS_FILE" ]; then
+        common_deps=$(grep "^common=" "$DEPS_FILE" 2>/dev/null | cut -d= -f2 || echo "")
+        service_deps=$(grep "^${service}=" "$DEPS_FILE" 2>/dev/null | cut -d= -f2 || echo "")
+    fi
+
+    echo "$common_deps $service_deps" | xargs
+}
+
+print_info "Creating Smartbotic packages version $BUILD_VERSION"
+print_info "Target:     $BUILD_TARGET"
+print_info "Build dir:  $BUILD_DIR"
+print_info "Source dir: $SOURCE_DIR"
+print_info "Output dir: $OUTPUT_DIR"
+echo ""
+
+# Full version string includes target
+FULL_VERSION="${BUILD_VERSION}-${BUILD_TARGET}"
+
+mkdir -p "$OUTPUT_DIR"
+
+# Create database package
+create_database_package() {
+    print_step "Creating smartbotic-database package..."
+
+    local pkg_name="smartbotic-database-$FULL_VERSION"
+    local pkg_dir="$OUTPUT_DIR/$pkg_name"
+
+    rm -rf "$pkg_dir"
+    mkdir -p "$pkg_dir"/{bin,etc/smartbotic,etc/systemd/system,share/smartbotic/database/migrations}
+
+    # Copy binary
+    local bin_path="$BUILD_DIR/external/smartbotic-database/service/smartbotic-database"
+    if [ -f "$bin_path" ]; then
+        print_info "  Copying binary..."
+        cp "$bin_path" "$pkg_dir/bin/smartbotic-database"
+        chmod 755 "$pkg_dir/bin/smartbotic-database"
+    else
+        print_info "  Warning: Binary not found at $bin_path"
+    fi
+
+    # Copy default config (storage.json -> database.json.default)
+    if [ -f "$SOURCE_DIR/config/storage.json" ]; then
+        print_info "  Copying config template..."
+        cp "$SOURCE_DIR/config/storage.json" "$pkg_dir/etc/smartbotic/database.json.default"
+    fi
+
+    # Copy systemd service
+    if [ -f "$SOURCE_DIR/packaging/systemd/smartbotic-database.service" ]; then
+        print_info "  Copying systemd unit..."
+        cp "$SOURCE_DIR/packaging/systemd/smartbotic-database.service" "$pkg_dir/etc/systemd/system/"
+    fi
+
+    # Copy migrations
+    if [ -d "$SOURCE_DIR/config/migrations" ]; then
+        print_info "  Copying database migrations..."
+        cp "$SOURCE_DIR/config/migrations"/*.json "$pkg_dir/share/smartbotic/database/migrations/" 2>/dev/null || true
+    fi
+
+    # Version file
+    echo "$BUILD_VERSION" > "$pkg_dir/VERSION"
+
+    # Dependencies file
+    local deps=$(get_deps "database")
+    if [ -n "$deps" ]; then
+        print_info "  Adding dependencies: $deps"
+        echo "$deps" > "$pkg_dir/DEPS"
+    fi
+
+    # Create tarball
+    tar -czf "$OUTPUT_DIR/$pkg_name.tar.gz" -C "$OUTPUT_DIR" "$pkg_name"
+    rm -rf "$pkg_dir"
+
+    print_info "  Created: $pkg_name.tar.gz"
+}
+
+# Create microbit package
+create_microbit_package() {
+    print_step "Creating smartbotic-microbit package..."
+
+    local pkg_name="smartbotic-microbit-$FULL_VERSION"
+    local pkg_dir="$OUTPUT_DIR/$pkg_name"
+
+    rm -rf "$pkg_dir"
+    mkdir -p "$pkg_dir"/{bin,etc/smartbotic,etc/systemd/system,share/smartbotic/nginx}
+
+    # Copy binary
+    local bin_path="$BUILD_DIR/src/smartbotic-microbit"
+    if [ -f "$bin_path" ]; then
+        print_info "  Copying binary..."
+        cp "$bin_path" "$pkg_dir/bin/smartbotic-microbit"
+        chmod 755 "$pkg_dir/bin/smartbotic-microbit"
+    else
+        print_info "  Warning: Binary not found at $bin_path"
+    fi
+
+    # Copy management scripts
+    for script in install.sh update.sh setup-nginx.sh; do
+        local src="$SOURCE_DIR/packaging/scripts/$script"
+        if [ -f "$src" ]; then
+            local dest_name="smartbotic-${script%.sh}"
+            print_info "  Copying $dest_name..."
+            cp "$src" "$pkg_dir/bin/$dest_name"
+            chmod 755 "$pkg_dir/bin/$dest_name"
+        fi
+    done
+
+    # Copy default config
+    if [ -f "$SOURCE_DIR/config/microbit.json" ]; then
+        print_info "  Copying config template..."
+        cp "$SOURCE_DIR/config/microbit.json" "$pkg_dir/etc/smartbotic/microbit.json.default"
+    fi
+
+    # Create environment template
+    print_info "  Creating environment template..."
+    cat > "$pkg_dir/etc/smartbotic/smartbotic.env.default" <<'EOF'
+# Smartbotic Environment Configuration
+# Copy this file to smartbotic.env and customize the values
+#
+# SECURITY: This file contains secrets - ensure proper permissions (chmod 640)
+
+# Database
+DB_ADDRESS=localhost:9004
+DB_DATA_DIR=/var/lib/smartbotic/database/storage
+DB_MIGRATIONS_DIR=/var/lib/smartbotic/database/migrations
+
+# Authentication (REQUIRED - generate secure random values)
+# Generate with: openssl rand -hex 32
+JWT_SECRET=CHANGE_ME_GENERATE_SECURE_SECRET
+
+# WebUI
+WEBUI_PATH=/var/lib/smartbotic/microbit/webui
+
+# SMTP (optional - for invitation emails)
+# SMTP_HOST=
+# SMTP_PORT=587
+# SMTP_USER=
+# SMTP_PASSWORD=
+# SMTP_FROM=noreply@smartbotics.ai
+
+# CallerAI API (required for voice AI features)
+# CALLERAI_API_URL=
+# CALLERAI_API_KEY=
+EOF
+
+    # Copy systemd service
+    if [ -f "$SOURCE_DIR/packaging/systemd/smartbotic-microbit.service" ]; then
+        print_info "  Copying systemd unit..."
+        cp "$SOURCE_DIR/packaging/systemd/smartbotic-microbit.service" "$pkg_dir/etc/systemd/system/"
+    fi
+
+    # Copy nginx template
+    if [ -f "$SOURCE_DIR/packaging/nginx/smartbotic.conf.template" ]; then
+        print_info "  Copying nginx template..."
+        cp "$SOURCE_DIR/packaging/nginx/smartbotic.conf.template" "$pkg_dir/share/smartbotic/nginx/"
+    fi
+
+    # Version file
+    echo "$BUILD_VERSION" > "$pkg_dir/VERSION"
+
+    # Dependencies file
+    local deps=$(get_deps "microbit")
+    if [ -n "$deps" ]; then
+        print_info "  Adding dependencies: $deps"
+        echo "$deps" > "$pkg_dir/DEPS"
+    fi
+
+    # Create tarball
+    tar -czf "$OUTPUT_DIR/$pkg_name.tar.gz" -C "$OUTPUT_DIR" "$pkg_name"
+    rm -rf "$pkg_dir"
+
+    print_info "  Created: $pkg_name.tar.gz"
+}
+
+# Create WebUI package
+create_webui_package() {
+    print_step "Creating smartbotic-webui package..."
+
+    local pkg_name="smartbotic-webui-$FULL_VERSION"
+    local pkg_dir="$OUTPUT_DIR/$pkg_name"
+
+    rm -rf "$pkg_dir"
+    mkdir -p "$pkg_dir/share/smartbotic/webui"
+
+    # Copy built WebUI
+    local webui_dist="$SOURCE_DIR/webui/dist"
+    if [ -d "$webui_dist" ]; then
+        print_info "  Copying WebUI files..."
+        cp -a "$webui_dist"/* "$pkg_dir/share/smartbotic/webui/"
+    else
+        print_info "  Warning: WebUI dist not found at $webui_dist"
+    fi
+
+    # Version file
+    echo "$BUILD_VERSION" > "$pkg_dir/VERSION"
+
+    # Create tarball
+    tar -czf "$OUTPUT_DIR/$pkg_name.tar.gz" -C "$OUTPUT_DIR" "$pkg_name"
+    rm -rf "$pkg_dir"
+
+    print_info "  Created: $pkg_name.tar.gz"
+}
+
+# Main execution
+main() {
+    create_database_package
+    create_microbit_package
+    create_webui_package
+
+    echo ""
+    print_info "All packages created successfully!"
+    echo ""
+    echo "Packages:"
+    ls -lh "$OUTPUT_DIR"/*.tar.gz
+}
+
+main

+ 555 - 0
packaging/scripts/install.sh

@@ -0,0 +1,555 @@
+#!/bin/bash
+# Smartbotic Service Installer
+# Installs Smartbotic service packages with config preservation and proper security
+#
+# Usage:
+#   sudo ./install.sh smartbotic-*.tar.gz
+#   sudo ./install.sh --dry-run smartbotic-database-*.tar.gz smartbotic-microbit-*.tar.gz
+
+set -e
+
+# Default paths
+PREFIX="/opt/smartbotic"
+BINDIR="$PREFIX/bin"
+CONFDIR="/etc/smartbotic"
+DATADIR="/var/lib/smartbotic"
+LOGDIR="/var/log/smartbotic"
+SYSTEMD_DIR="/etc/systemd/system"
+
+# Smartbotic system user
+SMARTBOTIC_USER="smartbotic"
+SMARTBOTIC_GROUP="smartbotic"
+
+# Log file (set after PREFIX is finalized)
+INSTALL_LOG=""
+
+# Output control
+VERBOSE=false
+QUIET=false
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+# Initialize logging
+init_logging() {
+    mkdir -p "$PREFIX"
+    INSTALL_LOG="$PREFIX/install-$(date +%Y%m%d-%H%M%S).log"
+    echo "Smartbotic Install Log - $(date)" > "$INSTALL_LOG"
+    echo "Command: $0 $ORIGINAL_ARGS" >> "$INSTALL_LOG"
+    echo "========================================" >> "$INSTALL_LOG"
+}
+
+# Log to file only
+log() {
+    echo "[$(date +%H:%M:%S)] $1" >> "$INSTALL_LOG"
+}
+
+# Print to console (respects quiet mode)
+print_info() {
+    log "INFO: $1"
+    if [ "$QUIET" = false ]; then
+        if [ "$VERBOSE" = true ]; then
+            echo -e "${GREEN}[INFO]${NC} $1"
+        fi
+    fi
+}
+
+print_warn() {
+    log "WARN: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "${YELLOW}[WARN]${NC} $1"
+    fi
+}
+
+print_error() {
+    log "ERROR: $1"
+    echo -e "${RED}[ERROR]${NC} $1" >&2
+}
+
+# Always show steps (progress indicator)
+print_step() {
+    log "STEP: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "${BLUE}▶${NC} $1"
+    fi
+}
+
+# Summary line (always shown unless quiet)
+print_summary() {
+    log "SUMMARY: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "  ${GREEN}✓${NC} $1"
+    fi
+}
+
+usage() {
+    cat <<EOF
+Smartbotic Service Installer
+
+Usage: $0 [OPTIONS] PACKAGE.tar.gz [PACKAGE2.tar.gz ...]
+
+Options:
+    --prefix DIR      Install prefix (default: /opt/smartbotic)
+    --confdir DIR     Config directory (default: /etc/smartbotic)
+    --user USER       Service user (default: smartbotic)
+    --group GROUP     Service group (default: smartbotic)
+    --no-systemd      Skip systemd service installation
+    --no-user         Skip user/group creation (use existing)
+    --no-deps         Skip apt dependency installation
+    --preserve-all    Preserve all existing files (no overwrites)
+    --force           Overwrite existing binaries
+    --dry-run         Show what would be done without making changes
+    -v, --verbose     Show detailed output
+    -q, --quiet       Minimal output (errors only)
+    -h, --help        Show this help
+
+Log file: Install details are logged to \$PREFIX/install-YYYYMMDD-HHMMSS.log
+
+Examples:
+    # Install all packages (minimal output)
+    sudo $0 smartbotic-*.tar.gz
+
+    # Install with verbose output
+    sudo $0 -v smartbotic-*.tar.gz
+
+    # Test installation
+    $0 --dry-run smartbotic-*.tar.gz
+EOF
+}
+
+# Parse arguments
+NO_SYSTEMD=false
+NO_USER=false
+NO_DEPS=false
+PRESERVE_ALL=false
+FORCE=false
+DRY_RUN=false
+PACKAGES=()
+ALL_DEPS=""
+ORIGINAL_ARGS="$*"
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --prefix)
+            PREFIX="$2"
+            BINDIR="$PREFIX/bin"
+            shift 2
+            ;;
+        --confdir)
+            CONFDIR="$2"
+            shift 2
+            ;;
+        --user)
+            SMARTBOTIC_USER="$2"
+            shift 2
+            ;;
+        --group)
+            SMARTBOTIC_GROUP="$2"
+            shift 2
+            ;;
+        --no-systemd)
+            NO_SYSTEMD=true
+            shift
+            ;;
+        --no-user)
+            NO_USER=true
+            shift
+            ;;
+        --no-deps)
+            NO_DEPS=true
+            shift
+            ;;
+        --preserve-all)
+            PRESERVE_ALL=true
+            shift
+            ;;
+        --force)
+            FORCE=true
+            shift
+            ;;
+        --dry-run)
+            DRY_RUN=true
+            shift
+            ;;
+        -v|--verbose)
+            VERBOSE=true
+            shift
+            ;;
+        -q|--quiet)
+            QUIET=true
+            shift
+            ;;
+        -h|--help)
+            usage
+            exit 0
+            ;;
+        -*)
+            print_error "Unknown option: $1"
+            usage
+            exit 1
+            ;;
+        *)
+            PACKAGES+=("$1")
+            shift
+            ;;
+    esac
+done
+
+if [ ${#PACKAGES[@]} -eq 0 ]; then
+    print_error "No packages specified"
+    usage
+    exit 1
+fi
+
+# Check root privileges
+if [ "$EUID" -ne 0 ] && [ "$DRY_RUN" = false ]; then
+    print_error "This script must be run as root (or use --dry-run)"
+    exit 1
+fi
+
+# Run command, log output
+run_cmd() {
+    log "CMD: $*"
+    if [ "$DRY_RUN" = true ]; then
+        log "  [DRY-RUN] Skipped"
+        if [ "$VERBOSE" = true ]; then
+            echo "  [DRY-RUN] $*"
+        fi
+    else
+        local output
+        if output=$("$@" 2>&1); then
+            if [ -n "$output" ]; then
+                echo "$output" >> "$INSTALL_LOG"
+            fi
+        else
+            local exit_code=$?
+            if [ -n "$output" ]; then
+                echo "$output" >> "$INSTALL_LOG"
+            fi
+            return $exit_code
+        fi
+    fi
+}
+
+# Collect dependencies from package
+collect_deps() {
+    local deps="$1"
+    if [ -n "$deps" ]; then
+        for dep in $deps; do
+            if [[ ! " $ALL_DEPS " =~ " $dep " ]]; then
+                ALL_DEPS="$ALL_DEPS $dep"
+            fi
+        done
+    fi
+}
+
+# Install all collected dependencies
+install_deps() {
+    if [ "$NO_DEPS" = true ]; then
+        print_info "Skipping dependency installation (--no-deps)"
+        return
+    fi
+
+    ALL_DEPS=$(echo "$ALL_DEPS" | xargs)
+
+    if [ -z "$ALL_DEPS" ]; then
+        print_info "No dependencies to install"
+        return
+    fi
+
+    print_step "Installing dependencies..."
+    log "Dependencies: $ALL_DEPS"
+
+    if [ "$DRY_RUN" = true ]; then
+        log "[DRY-RUN] apt-get update && apt-get install -y $ALL_DEPS"
+    else
+        apt-get update -qq >> "$INSTALL_LOG" 2>&1
+        apt-get install -y --no-install-recommends $ALL_DEPS >> "$INSTALL_LOG" 2>&1
+    fi
+    print_summary "Dependencies installed"
+}
+
+# Create system user and group
+create_user() {
+    if [ "$NO_USER" = true ]; then
+        print_info "Skipping user creation (--no-user)"
+        return
+    fi
+
+    print_step "Setting up system user..."
+
+    if ! getent group "$SMARTBOTIC_GROUP" > /dev/null 2>&1; then
+        log "Creating group: $SMARTBOTIC_GROUP"
+        run_cmd groupadd --system "$SMARTBOTIC_GROUP"
+    fi
+
+    if ! getent passwd "$SMARTBOTIC_USER" > /dev/null 2>&1; then
+        log "Creating user: $SMARTBOTIC_USER"
+        run_cmd useradd --system \
+            --gid "$SMARTBOTIC_GROUP" \
+            --home-dir "$DATADIR" \
+            --shell /usr/sbin/nologin \
+            --comment "Smartbotic Service User" \
+            "$SMARTBOTIC_USER"
+    fi
+    print_summary "User $SMARTBOTIC_USER ready"
+}
+
+# Create directory structure
+create_directories() {
+    print_step "Creating directories..."
+
+    local dirs=(
+        "$PREFIX"
+        "$BINDIR"
+        "$PREFIX/share/smartbotic/nginx"
+        "$CONFDIR"
+        "$DATADIR"
+        "$DATADIR/database"
+        "$DATADIR/database/storage"
+        "$DATADIR/database/migrations"
+        "$DATADIR/microbit"
+        "$DATADIR/microbit/webui"
+        "$LOGDIR"
+    )
+
+    for dir in "${dirs[@]}"; do
+        if [ ! -d "$dir" ]; then
+            log "Creating: $dir"
+            run_cmd mkdir -p "$dir"
+        fi
+    done
+
+    if [ "$DRY_RUN" = false ]; then
+        chown -R "$SMARTBOTIC_USER:$SMARTBOTIC_GROUP" "$DATADIR" "$LOGDIR"
+        chown root:"$SMARTBOTIC_GROUP" "$CONFDIR"
+        chmod 750 "$CONFDIR"
+    fi
+    print_summary "Directories created"
+}
+
+# Install a single package
+install_package() {
+    local pkg="$1"
+    local pkg_basename=$(basename "$pkg")
+    local pkg_name="${pkg_basename%.tar.gz}"
+    # Extract short name (e.g., "database" from "smartbotic-database-0.1.0-debian13")
+    local short_name=$(echo "$pkg_name" | sed -E 's/smartbotic-([^-]+).*/\1/')
+
+    print_step "Installing $short_name..."
+
+    local tmpdir
+    tmpdir=$(mktemp -d)
+    trap "rm -rf '$tmpdir'" RETURN
+
+    tar -xzf "$pkg" -C "$tmpdir"
+
+    local pkg_root="$tmpdir/$pkg_name"
+    if [ ! -d "$pkg_root" ]; then
+        pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1)
+    fi
+
+    if [ ! -d "$pkg_root" ]; then
+        print_error "Could not find package root in archive"
+        return 1
+    fi
+
+    # Collect dependencies
+    if [ -f "$pkg_root/DEPS" ]; then
+        collect_deps "$(cat "$pkg_root/DEPS")"
+    fi
+
+    local installed_items=()
+
+    # Install binaries
+    if [ -d "$pkg_root/bin" ]; then
+        for binary in "$pkg_root/bin"/*; do
+            if [ -f "$binary" ]; then
+                local bin_name=$(basename "$binary")
+                local dest="$BINDIR/$bin_name"
+
+                if [ -f "$dest" ] && [ "$FORCE" = false ] && [ "$PRESERVE_ALL" = true ]; then
+                    log "Skipping existing: $dest"
+                else
+                    log "Installing binary: $dest"
+                    run_cmd install -m 755 "$binary" "$dest"
+                    installed_items+=("binary")
+                fi
+            fi
+        done
+    fi
+
+    # Install configs (never overwrite existing)
+    if [ -d "$pkg_root/etc/smartbotic" ]; then
+        for cfg in "$pkg_root/etc/smartbotic"/*; do
+            if [ -f "$cfg" ]; then
+                local cfg_name=$(basename "$cfg")
+                local dest_name="${cfg_name%.default}"
+                local dest="$CONFDIR/$dest_name"
+
+                if [ -f "$dest" ]; then
+                    log "Config exists, preserving: $dest"
+                    if [ "$DRY_RUN" = false ]; then
+                        cp "$cfg" "$dest.new"
+                        chown root:"$SMARTBOTIC_GROUP" "$dest.new"
+                        chmod 640 "$dest.new"
+                    fi
+                else
+                    log "Installing config: $dest"
+                    run_cmd install -m 640 -o root -g "$SMARTBOTIC_GROUP" "$cfg" "$dest"
+                    installed_items+=("config")
+                fi
+            fi
+        done
+    fi
+
+    # Install systemd services
+    if [ -d "$pkg_root/etc/systemd/system" ] && [ "$NO_SYSTEMD" = false ]; then
+        for service in "$pkg_root/etc/systemd/system"/*.service; do
+            if [ -f "$service" ]; then
+                local svc_name=$(basename "$service")
+                log "Installing systemd service: $svc_name"
+                run_cmd install -m 644 "$service" "$SYSTEMD_DIR/$svc_name"
+                installed_items+=("service")
+            fi
+        done
+    fi
+
+    # Install data files
+    if [ -d "$pkg_root/share/smartbotic" ]; then
+        log "Installing data files..."
+        if [ "$DRY_RUN" = false ]; then
+            # WebUI files
+            if [ -d "$pkg_root/share/smartbotic/webui" ]; then
+                rm -rf "$DATADIR/microbit/webui"/*
+                cp -a "$pkg_root/share/smartbotic/webui"/* "$DATADIR/microbit/webui/" 2>/dev/null || true
+                installed_items+=("webui")
+            fi
+
+            # Database migrations
+            if [ -d "$pkg_root/share/smartbotic/database/migrations" ]; then
+                cp -a "$pkg_root/share/smartbotic/database/migrations"/* "$DATADIR/database/migrations/" 2>/dev/null || true
+                installed_items+=("migrations")
+            fi
+
+            # Nginx template
+            if [ -d "$pkg_root/share/smartbotic/nginx" ]; then
+                cp -a "$pkg_root/share/smartbotic/nginx"/* "$PREFIX/share/smartbotic/nginx/" 2>/dev/null || true
+                installed_items+=("nginx")
+            fi
+
+            chown -R "$SMARTBOTIC_USER:$SMARTBOTIC_GROUP" "$DATADIR"
+        fi
+    fi
+
+    print_summary "$short_name installed"
+}
+
+# Main installation
+main() {
+    # Initialize logging first
+    init_logging
+
+    if [ "$QUIET" = false ]; then
+        echo ""
+        echo -e "${GREEN}Smartbotic Installer${NC}"
+        echo "===================="
+    fi
+
+    log "PREFIX=$PREFIX"
+    log "CONFDIR=$CONFDIR"
+    log "DATADIR=$DATADIR"
+    log "USER=$SMARTBOTIC_USER"
+    log "PACKAGES=${#PACKAGES[@]}"
+
+    if [ "$DRY_RUN" = true ]; then
+        print_warn "DRY-RUN MODE - no changes will be made"
+    fi
+
+    # Create user and directories
+    create_user
+    create_directories
+
+    # Sort packages: database first, then microbit, then webui, then others
+    local sorted_packages=()
+    local database_pkg=""
+    local microbit_pkg=""
+    local webui_pkg=""
+    local other_packages=()
+
+    for pkg in "${PACKAGES[@]}"; do
+        case "$pkg" in
+            *smartbotic-database*) database_pkg="$pkg" ;;
+            *smartbotic-microbit*) microbit_pkg="$pkg" ;;
+            *smartbotic-webui*)    webui_pkg="$pkg" ;;
+            *)                     other_packages+=("$pkg") ;;
+        esac
+    done
+
+    [ -n "$database_pkg" ] && sorted_packages+=("$database_pkg")
+    [ -n "$microbit_pkg" ] && sorted_packages+=("$microbit_pkg")
+    [ -n "$webui_pkg" ] && sorted_packages+=("$webui_pkg")
+    sorted_packages+=("${other_packages[@]}")
+
+    # First pass: collect dependencies
+    for pkg in "${sorted_packages[@]}"; do
+        if [ -f "$pkg" ]; then
+            local tmpdir=$(mktemp -d)
+            tar -xzf "$pkg" -C "$tmpdir" 2>/dev/null
+            local pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1)
+            if [ -f "$pkg_root/DEPS" ]; then
+                collect_deps "$(cat "$pkg_root/DEPS")"
+            fi
+            rm -rf "$tmpdir"
+        fi
+    done
+
+    # Install dependencies
+    install_deps
+
+    # Install packages
+    for pkg in "${sorted_packages[@]}"; do
+        if [ ! -f "$pkg" ]; then
+            print_error "Package not found: $pkg"
+            continue
+        fi
+        install_package "$pkg"
+    done
+
+    # Post-installation
+    print_step "Finalizing..."
+
+    if [ "$NO_SYSTEMD" = false ] && [ "$DRY_RUN" = false ]; then
+        log "Reloading systemd daemon..."
+        systemctl daemon-reload >> "$INSTALL_LOG" 2>&1
+    fi
+
+    print_summary "Installation complete"
+
+    if [ "$QUIET" = false ]; then
+        echo ""
+        echo -e "${GREEN}Done!${NC} Log: $INSTALL_LOG"
+        echo ""
+        echo "Next steps:"
+        echo "  1. Configure: $CONFDIR/smartbotic.env"
+        echo "     (copy from $CONFDIR/smartbotic.env.default if not exists)"
+        echo "     Set JWT_SECRET, SMTP, and CallerAI API settings"
+        echo "  2. Review configs: $CONFDIR/database.json, $CONFDIR/microbit.json"
+        echo "  3. Enable services:"
+        echo "     systemctl enable smartbotic-database smartbotic-microbit"
+        echo "  4. Start services:"
+        echo "     systemctl start smartbotic-database"
+        echo "     systemctl start smartbotic-microbit"
+        echo "  5. Setup nginx (optional):"
+        echo "     $BINDIR/smartbotic-setup-nginx --domain example.com --email admin@example.com"
+        echo ""
+    fi
+
+    log "Installation completed successfully"
+}
+
+main

+ 435 - 0
packaging/scripts/setup-nginx.sh

@@ -0,0 +1,435 @@
+#!/bin/bash
+# Smartbotic Nginx + SSL Setup Script
+# Sets up nginx as reverse proxy with Let's Encrypt SSL
+#
+# Usage:
+#   sudo ./setup-nginx.sh --domain console.example.com --email admin@example.com
+#   sudo ./setup-nginx.sh --domain console.example.com --email admin@example.com --staging
+
+set -e
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
+print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
+print_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
+
+usage() {
+    cat <<EOF
+Smartbotic Nginx + SSL Setup
+
+Usage: $0 [OPTIONS]
+
+Required:
+    --domain DOMAIN     Domain name(s) for SSL certificate. Can be specified multiple times
+                        or as comma-separated list (e.g., --domain app.example.com,api.example.com)
+    --email EMAIL       Email for Let's Encrypt notifications
+
+Options:
+    --staging           Use Let's Encrypt staging server (for testing)
+    --no-ssl            Skip SSL setup (HTTP only)
+    --nginx-only        Only install/configure nginx, skip certbot
+    --renew             Renew existing certificates
+    --dry-run           Show what would be done
+    -h, --help          Show this help
+
+Examples:
+    # Single domain with SSL
+    sudo $0 --domain app.smartbotics.ai --email admin@smartbotics.ai
+
+    # Multiple domains (comma-separated)
+    sudo $0 --domain app.smartbotics.ai,api.smartbotics.ai --email admin@smartbotics.ai
+
+    # Test with staging certificates first
+    sudo $0 --domain app.smartbotics.ai --email admin@smartbotics.ai --staging
+
+    # Renew certificates
+    sudo $0 --renew
+EOF
+}
+
+DOMAINS=()
+EMAIL=""
+STAGING=false
+NO_SSL=false
+NGINX_ONLY=false
+RENEW=false
+DRY_RUN=false
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --domain)
+            # Support comma-separated domains
+            IFS=',' read -ra NEW_DOMAINS <<< "$2"
+            for d in "${NEW_DOMAINS[@]}"; do
+                # Trim whitespace
+                d=$(echo "$d" | xargs)
+                [ -n "$d" ] && DOMAINS+=("$d")
+            done
+            shift 2
+            ;;
+        --email) EMAIL="$2"; shift 2 ;;
+        --staging) STAGING=true; shift ;;
+        --no-ssl) NO_SSL=true; shift ;;
+        --nginx-only) NGINX_ONLY=true; shift ;;
+        --renew) RENEW=true; shift ;;
+        --dry-run) DRY_RUN=true; shift ;;
+        -h|--help) usage; exit 0 ;;
+        *) print_error "Unknown option: $1"; usage; exit 1 ;;
+    esac
+done
+
+# Primary domain is the first one (used for certificate directory)
+PRIMARY_DOMAIN="${DOMAINS[0]:-}"
+
+# Build server_name string (space-separated for nginx)
+SERVER_NAMES="${DOMAINS[*]}"
+
+# Build certbot domain args (-d domain1 -d domain2 ...)
+CERTBOT_DOMAINS=""
+for d in "${DOMAINS[@]}"; do
+    CERTBOT_DOMAINS="$CERTBOT_DOMAINS -d $d"
+done
+
+# Validate arguments
+if [ "$RENEW" = false ]; then
+    if [ ${#DOMAINS[@]} -eq 0 ]; then
+        print_error "--domain is required"
+        usage
+        exit 1
+    fi
+    if [ -z "$EMAIL" ] && [ "$NO_SSL" = false ] && [ "$NGINX_ONLY" = false ]; then
+        print_error "--email is required for SSL setup"
+        usage
+        exit 1
+    fi
+fi
+
+# Check root
+if [ "$EUID" -ne 0 ] && [ "$DRY_RUN" = false ]; then
+    print_error "Must run as root"
+    exit 1
+fi
+
+# Paths
+NGINX_CONF="/etc/nginx/sites-available/smartbotic"
+NGINX_ENABLED="/etc/nginx/sites-enabled/smartbotic"
+CERTBOT_WEBROOT="/var/www/certbot"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+TEMPLATE_DIR="$(dirname "$SCRIPT_DIR")/nginx"
+
+# Find template
+find_template() {
+    local template=""
+    local search_paths=(
+        "$TEMPLATE_DIR/smartbotic.conf.template"
+        "/opt/smartbotic/share/smartbotic/nginx/smartbotic.conf.template"
+        "./smartbotic.conf.template"
+    )
+    for path in "${search_paths[@]}"; do
+        if [ -f "$path" ]; then
+            template="$path"
+            break
+        fi
+    done
+    echo "$template"
+}
+
+run_cmd() {
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] $*"
+    else
+        "$@"
+    fi
+}
+
+# Install nginx
+install_nginx() {
+    print_step "Installing nginx..."
+
+    if command -v nginx &> /dev/null; then
+        print_info "  nginx is already installed"
+        return
+    fi
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] apt-get update && apt-get install -y nginx"
+    else
+        apt-get update -qq
+        apt-get install -y nginx
+    fi
+}
+
+# Install certbot
+install_certbot() {
+    print_step "Installing certbot..."
+
+    if command -v certbot &> /dev/null; then
+        print_info "  certbot is already installed"
+        return
+    fi
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] apt-get install -y certbot python3-certbot-nginx"
+    else
+        apt-get update -qq
+        apt-get install -y certbot python3-certbot-nginx
+    fi
+}
+
+# Create initial HTTP-only config for certificate issuance
+create_http_config() {
+    print_step "Creating initial HTTP configuration..."
+
+    run_cmd mkdir -p "$CERTBOT_WEBROOT"
+
+    local http_config="# Temporary HTTP config for Let's Encrypt challenge
+server {
+    listen 80;
+    listen [::]:80;
+    server_name $SERVER_NAMES;
+
+    location /.well-known/acme-challenge/ {
+        root $CERTBOT_WEBROOT;
+    }
+
+    location / {
+        return 503 'SSL certificate pending';
+        add_header Content-Type text/plain;
+    }
+}
+"
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] Creating $NGINX_CONF"
+        echo "$http_config"
+    else
+        echo "$http_config" > "$NGINX_CONF"
+        ln -sf "$NGINX_CONF" "$NGINX_ENABLED" 2>/dev/null || true
+        # Remove default site if exists
+        rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
+        nginx -t && systemctl reload nginx
+    fi
+}
+
+# Obtain SSL certificate
+obtain_certificate() {
+    print_step "Obtaining SSL certificate from Let's Encrypt..."
+    print_info "  Domains: $SERVER_NAMES"
+
+    local certbot_args="certonly --webroot -w $CERTBOT_WEBROOT $CERTBOT_DOMAINS --email $EMAIL --agree-tos --non-interactive"
+
+    if [ "$STAGING" = true ]; then
+        certbot_args="$certbot_args --staging"
+        print_warn "  Using Let's Encrypt STAGING server (certificates won't be trusted)"
+    fi
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] certbot $certbot_args"
+    else
+        certbot $certbot_args
+    fi
+}
+
+# Create full HTTPS config
+create_https_config() {
+    print_step "Creating HTTPS configuration..."
+
+    local template
+    template=$(find_template)
+
+    if [ -z "$template" ]; then
+        print_error "Cannot find nginx template file"
+        print_info "Expected locations:"
+        print_info "  - $TEMPLATE_DIR/smartbotic.conf.template"
+        print_info "  - /opt/smartbotic/share/smartbotic/nginx/smartbotic.conf.template"
+        exit 1
+    fi
+
+    print_info "  Using template: $template"
+    print_info "  Primary domain: $PRIMARY_DOMAIN"
+    print_info "  All domains: $SERVER_NAMES"
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] Generating config from template"
+    else
+        # Replace placeholders
+        sed -e "s/{{DOMAIN}}/$PRIMARY_DOMAIN/g" \
+            -e "s/{{SERVER_NAMES}}/$SERVER_NAMES/g" \
+            "$template" > "$NGINX_CONF"
+        nginx -t && systemctl reload nginx
+    fi
+}
+
+# Create HTTP-only config (no SSL)
+create_http_only_config() {
+    print_step "Creating HTTP-only configuration (no SSL)..."
+
+    local http_config="# Smartbotic Nginx Configuration (HTTP only)
+# Domains: $SERVER_NAMES
+
+upstream smartbotic_http {
+    server 127.0.0.1:8090;
+    keepalive 32;
+}
+
+server {
+    listen 80;
+    listen [::]:80;
+    server_name $SERVER_NAMES;
+
+    access_log /var/log/nginx/smartbotic_access.log;
+    error_log /var/log/nginx/smartbotic_error.log;
+
+    client_max_body_size 50M;
+
+    # Gzip compression
+    gzip on;
+    gzip_vary on;
+    gzip_proxied any;
+    gzip_comp_level 6;
+    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
+
+    location /api/ {
+        proxy_pass http://smartbotic_http;
+        proxy_http_version 1.1;
+        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;
+        proxy_set_header Connection \"\";
+        proxy_read_timeout 300s;
+        proxy_buffering off;
+    }
+
+    location / {
+        proxy_pass http://smartbotic_http;
+        proxy_http_version 1.1;
+        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;
+        proxy_set_header Connection \"\";
+    }
+}
+"
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] Creating $NGINX_CONF"
+    else
+        echo "$http_config" > "$NGINX_CONF"
+        ln -sf "$NGINX_CONF" "$NGINX_ENABLED" 2>/dev/null || true
+        rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
+        nginx -t && systemctl reload nginx
+    fi
+}
+
+# Setup certificate renewal
+setup_renewal() {
+    print_step "Setting up automatic certificate renewal..."
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] Configuring certbot renewal timer"
+    else
+        systemctl enable certbot.timer 2>/dev/null || true
+        systemctl start certbot.timer 2>/dev/null || true
+        print_info "  Certbot renewal timer enabled"
+    fi
+}
+
+# Renew certificates
+renew_certificates() {
+    print_step "Renewing certificates..."
+
+    if [ "$DRY_RUN" = true ]; then
+        echo "  [DRY-RUN] certbot renew"
+    else
+        certbot renew
+        systemctl reload nginx
+    fi
+}
+
+# Main
+main() {
+    echo ""
+    print_info "Smartbotic Nginx + SSL Setup"
+    print_info "============================"
+    if [ ${#DOMAINS[@]} -gt 0 ]; then
+        print_info "Domains: $SERVER_NAMES"
+    fi
+    if [ -n "$EMAIL" ]; then
+        print_info "Email:   $EMAIL"
+    fi
+    if [ "$STAGING" = true ]; then
+        print_warn "Mode:    STAGING (test certificates)"
+    fi
+    if [ "$DRY_RUN" = true ]; then
+        print_warn "DRY-RUN MODE - no changes will be made"
+    fi
+    echo ""
+
+    if [ "$RENEW" = true ]; then
+        renew_certificates
+        echo ""
+        print_info "Certificate renewal complete!"
+        exit 0
+    fi
+
+    # Install packages
+    install_nginx
+
+    if [ "$NO_SSL" = true ]; then
+        # HTTP only mode
+        create_http_only_config
+        echo ""
+        print_info "Nginx configured (HTTP only)"
+        print_info "Access WebUI at: http://$PRIMARY_DOMAIN/"
+    elif [ "$NGINX_ONLY" = true ]; then
+        # Just nginx, no certbot
+        create_http_only_config
+        echo ""
+        print_info "Nginx installed and configured"
+        print_info "Run with --email to set up SSL later"
+    else
+        # Full SSL setup
+        install_certbot
+
+        # Check if certificate already exists
+        if [ -d "/etc/letsencrypt/live/$PRIMARY_DOMAIN" ] && [ "$DRY_RUN" = false ]; then
+            print_info "Certificate already exists for $PRIMARY_DOMAIN"
+            create_https_config
+        else
+            create_http_config
+            obtain_certificate
+            create_https_config
+        fi
+
+        setup_renewal
+
+        echo ""
+        print_info "Setup complete!"
+        echo ""
+        echo "Your Smartbotic instance is now available at:"
+        for d in "${DOMAINS[@]}"; do
+            echo "  https://$d/"
+        done
+        echo ""
+        echo "Certificate renewal is automatic via certbot timer."
+        echo "To manually renew: sudo certbot renew"
+        echo ""
+    fi
+
+    # Enable and start nginx
+    if [ "$DRY_RUN" = false ]; then
+        systemctl enable nginx
+        systemctl start nginx 2>/dev/null || systemctl reload nginx
+    fi
+}
+
+main

+ 489 - 0
packaging/scripts/update.sh

@@ -0,0 +1,489 @@
+#!/bin/bash
+# Smartbotic Update Script
+# Updates Smartbotic services while preserving all configurations
+#
+# Usage:
+#   sudo ./update.sh /path/to/packages/
+#   sudo ./update.sh --rolling --backup /path/to/packages/
+
+set -e
+
+# Default paths (must match installation)
+PREFIX="${PREFIX:-/opt/smartbotic}"
+BINDIR="$PREFIX/bin"
+CONFDIR="${CONFDIR:-/etc/smartbotic}"
+DATADIR="${DATADIR:-/var/lib/smartbotic}"
+BACKUP_DIR="/var/backups/smartbotic"
+
+# Log file
+UPDATE_LOG=""
+
+# Output control
+VERBOSE=false
+QUIET=false
+
+# Service order (database first - it's a dependency)
+SERVICES=(
+    "smartbotic-database"
+    "smartbotic-microbit"
+)
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+# Initialize logging
+init_logging() {
+    mkdir -p "$PREFIX"
+    UPDATE_LOG="$PREFIX/update-$(date +%Y%m%d-%H%M%S).log"
+    echo "Smartbotic Update Log - $(date)" > "$UPDATE_LOG"
+    echo "Command: $0 $ORIGINAL_ARGS" >> "$UPDATE_LOG"
+    echo "========================================" >> "$UPDATE_LOG"
+}
+
+# Log to file only
+log() {
+    echo "[$(date +%H:%M:%S)] $1" >> "$UPDATE_LOG"
+}
+
+# Print to console (respects quiet mode)
+print_info() {
+    log "INFO: $1"
+    if [ "$QUIET" = false ] && [ "$VERBOSE" = true ]; then
+        echo -e "${GREEN}[INFO]${NC} $1"
+    fi
+}
+
+print_warn() {
+    log "WARN: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "${YELLOW}[WARN]${NC} $1"
+    fi
+}
+
+print_error() {
+    log "ERROR: $1"
+    echo -e "${RED}[ERROR]${NC} $1" >&2
+}
+
+# Always show steps (progress indicator)
+print_step() {
+    log "STEP: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "${BLUE}▶${NC} $1"
+    fi
+}
+
+# Summary line (always shown unless quiet)
+print_summary() {
+    log "SUMMARY: $1"
+    if [ "$QUIET" = false ]; then
+        echo -e "  ${GREEN}✓${NC} $1"
+    fi
+}
+
+usage() {
+    cat <<EOF
+Smartbotic Update Script
+
+Usage: $0 [OPTIONS] PACKAGE_DIR
+
+Options:
+    --rolling         Rolling update (one service at a time, minimizes downtime)
+    --backup          Create backup before update
+    --skip-restart    Update files but don't restart services
+    --no-deps         Skip dependency installation
+    --dry-run         Show what would be done
+    -v, --verbose     Show detailed output
+    -q, --quiet       Minimal output (errors only)
+    -h, --help        Show this help
+
+Log file: Update details are logged to \$PREFIX/update-YYYYMMDD-HHMMSS.log
+
+Examples:
+    # Standard update (stops all, updates, starts all)
+    sudo $0 /tmp/packages/
+
+    # Rolling update with backup (recommended for production)
+    sudo $0 --rolling --backup /tmp/packages/
+
+    # Update without restarting (for manual control)
+    sudo $0 --skip-restart /tmp/packages/
+EOF
+}
+
+ROLLING=false
+BACKUP=false
+DRY_RUN=false
+SKIP_RESTART=false
+NO_DEPS=false
+PACKAGE_DIR=""
+ALL_DEPS=""
+ORIGINAL_ARGS="$*"
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --rolling) ROLLING=true; shift ;;
+        --backup) BACKUP=true; shift ;;
+        --dry-run) DRY_RUN=true; shift ;;
+        --skip-restart) SKIP_RESTART=true; shift ;;
+        --no-deps) NO_DEPS=true; shift ;;
+        -v|--verbose) VERBOSE=true; shift ;;
+        -q|--quiet) QUIET=true; shift ;;
+        -h|--help) usage; exit 0 ;;
+        -*) print_error "Unknown option: $1"; usage; exit 1 ;;
+        *) PACKAGE_DIR="$1"; shift ;;
+    esac
+done
+
+if [ -z "$PACKAGE_DIR" ]; then
+    print_error "Package directory required"
+    usage
+    exit 1
+fi
+
+if [ ! -d "$PACKAGE_DIR" ]; then
+    print_error "Package directory does not exist: $PACKAGE_DIR"
+    exit 1
+fi
+
+# Check root
+if [ "$EUID" -ne 0 ] && [ "$DRY_RUN" = false ]; then
+    print_error "Must run as root (or use --dry-run)"
+    exit 1
+fi
+
+# Run command, log output
+run_cmd() {
+    log "CMD: $*"
+    if [ "$DRY_RUN" = true ]; then
+        log "  [DRY-RUN] Skipped"
+        if [ "$VERBOSE" = true ]; then
+            echo "  [DRY-RUN] $*"
+        fi
+    else
+        local output
+        if output=$("$@" 2>&1); then
+            if [ -n "$output" ]; then
+                echo "$output" >> "$UPDATE_LOG"
+            fi
+        else
+            local exit_code=$?
+            if [ -n "$output" ]; then
+                echo "$output" >> "$UPDATE_LOG"
+            fi
+            return $exit_code
+        fi
+    fi
+}
+
+# Collect dependencies from package
+collect_deps() {
+    local deps="$1"
+    if [ -n "$deps" ]; then
+        for dep in $deps; do
+            if [[ ! " $ALL_DEPS " =~ " $dep " ]]; then
+                ALL_DEPS="$ALL_DEPS $dep"
+            fi
+        done
+    fi
+}
+
+# Install new dependencies
+install_deps() {
+    if [ "$NO_DEPS" = true ]; then
+        return
+    fi
+
+    ALL_DEPS=$(echo "$ALL_DEPS" | xargs)
+    if [ -z "$ALL_DEPS" ]; then
+        return
+    fi
+
+    print_step "Installing dependencies..."
+    log "Dependencies: $ALL_DEPS"
+
+    if [ "$DRY_RUN" = true ]; then
+        log "[DRY-RUN] apt-get update && apt-get install -y $ALL_DEPS"
+    else
+        apt-get update -qq >> "$UPDATE_LOG" 2>&1
+        apt-get install -y --no-install-recommends $ALL_DEPS >> "$UPDATE_LOG" 2>&1
+    fi
+    print_summary "Dependencies installed"
+}
+
+# Create backup
+create_backup() {
+    if [ "$BACKUP" = false ]; then
+        return
+    fi
+
+    local backup_name="backup-$(date +%Y%m%d-%H%M%S)"
+    local backup_path="$BACKUP_DIR/$backup_name"
+
+    print_step "Creating backup..."
+    log "Backup path: $backup_path"
+
+    run_cmd mkdir -p "$backup_path"
+    run_cmd cp -a "$PREFIX" "$backup_path/opt-smartbotic"
+    run_cmd cp -a "$CONFDIR" "$backup_path/etc-smartbotic"
+
+    # Save current versions
+    if [ "$DRY_RUN" = false ]; then
+        for service in "${SERVICES[@]}"; do
+            local binary="$BINDIR/$service"
+            if [ -f "$binary" ]; then
+                echo "$service: $(md5sum "$binary" | cut -d' ' -f1)" >> "$backup_path/versions.txt"
+            fi
+        done
+    fi
+
+    print_summary "Backup: $backup_path"
+}
+
+# Stop a service gracefully
+stop_service() {
+    local service="$1"
+
+    if systemctl is-active --quiet "$service" 2>/dev/null; then
+        log "Stopping $service..."
+        run_cmd systemctl stop "$service"
+        if [ "$DRY_RUN" = false ]; then
+            sleep 2
+        fi
+    fi
+}
+
+# Start a service
+start_service() {
+    local service="$1"
+
+    if systemctl is-enabled --quiet "$service" 2>/dev/null; then
+        log "Starting $service..."
+        run_cmd systemctl start "$service"
+
+        if [ "$DRY_RUN" = false ]; then
+            sleep 3
+            if ! systemctl is-active --quiet "$service"; then
+                print_error "$service failed to start! Check: journalctl -u $service -n 50"
+                log "Service $service failed to start"
+                return 1
+            fi
+            log "$service started successfully"
+        fi
+    else
+        log "$service is not enabled, skipping start"
+    fi
+}
+
+# Update a single service
+update_service() {
+    local service="$1"
+    local service_name="${service#smartbotic-}"
+
+    local pkg
+    pkg=$(find "$PACKAGE_DIR" -name "$service-*.tar.gz" -type f | head -1)
+
+    if [ -z "$pkg" ]; then
+        log "No package found for $service, skipping"
+        return 0
+    fi
+
+    print_step "Updating $service_name..."
+
+    if [ "$ROLLING" = true ] && [ "$SKIP_RESTART" = false ]; then
+        stop_service "$service"
+    fi
+
+    local tmpdir
+    tmpdir=$(mktemp -d)
+
+    tar -xzf "$pkg" -C "$tmpdir"
+    local pkg_root
+    pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1)
+
+    # Update binary
+    if [ -f "$pkg_root/bin/$service" ]; then
+        log "Installing new binary..."
+        run_cmd install -m 755 "$pkg_root/bin/$service" "$BINDIR/$service"
+    fi
+
+    # Update management scripts
+    for script_bin in "$pkg_root/bin"/smartbotic-install "$pkg_root/bin"/smartbotic-update "$pkg_root/bin"/smartbotic-setup-nginx; do
+        if [ -f "$script_bin" ]; then
+            local script_name=$(basename "$script_bin")
+            log "Updating script: $script_name"
+            run_cmd install -m 755 "$script_bin" "$BINDIR/$script_name"
+        fi
+    done
+
+    # Update systemd service file
+    if [ -d "$pkg_root/etc/systemd/system" ]; then
+        for svc_file in "$pkg_root/etc/systemd/system"/*.service; do
+            if [ -f "$svc_file" ]; then
+                log "Updating systemd service..."
+                run_cmd install -m 644 "$svc_file" "/etc/systemd/system/"
+            fi
+        done
+    fi
+
+    # Check for new config defaults (never overwrite)
+    if [ -d "$pkg_root/etc/smartbotic" ]; then
+        for cfg in "$pkg_root/etc/smartbotic"/*; do
+            local cfg_name=$(basename "$cfg")
+            local dest_name="${cfg_name%.default}"
+            local dest="$CONFDIR/$dest_name"
+
+            if [ -f "$dest" ]; then
+                if ! diff -q "$cfg" "$dest" > /dev/null 2>&1; then
+                    log "New config default available: $dest_name.new"
+                    run_cmd cp "$cfg" "$dest.new"
+                fi
+            fi
+        done
+    fi
+
+    # Update data files (migrations, nginx template)
+    if [ -d "$pkg_root/share/smartbotic" ]; then
+        if [ -d "$pkg_root/share/smartbotic/database/migrations" ] && [ "$DRY_RUN" = false ]; then
+            log "Updating database migrations..."
+            cp -a "$pkg_root/share/smartbotic/database/migrations"/* "$DATADIR/database/migrations/" 2>/dev/null || true
+            chown -R smartbotic:smartbotic "$DATADIR/database/migrations"
+        fi
+        if [ -d "$pkg_root/share/smartbotic/nginx" ] && [ "$DRY_RUN" = false ]; then
+            log "Updating nginx template..."
+            cp -a "$pkg_root/share/smartbotic/nginx"/* "$PREFIX/share/smartbotic/nginx/" 2>/dev/null || true
+        fi
+    fi
+
+    rm -rf "$tmpdir"
+
+    if [ "$ROLLING" = true ] && [ "$SKIP_RESTART" = false ]; then
+        start_service "$service"
+    fi
+
+    print_summary "$service_name updated"
+}
+
+# Update WebUI
+update_webui() {
+    local pkg
+    pkg=$(find "$PACKAGE_DIR" -name "smartbotic-webui-*.tar.gz" -type f | head -1)
+
+    if [ -z "$pkg" ]; then
+        log "No WebUI package found, skipping"
+        return
+    fi
+
+    print_step "Updating webui..."
+
+    local tmpdir
+    tmpdir=$(mktemp -d)
+    trap "rm -rf '$tmpdir'" RETURN
+
+    tar -xzf "$pkg" -C "$tmpdir"
+    local pkg_root
+    pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1)
+
+    if [ -d "$pkg_root/share/smartbotic/webui" ]; then
+        log "Installing WebUI files..."
+        if [ "$DRY_RUN" = false ]; then
+            rm -rf "$DATADIR/microbit/webui"/*
+            cp -a "$pkg_root/share/smartbotic/webui"/* "$DATADIR/microbit/webui/"
+            chown -R smartbotic:smartbotic "$DATADIR/microbit/webui"
+        fi
+    fi
+
+    print_summary "WebUI updated"
+}
+
+# Main
+main() {
+    # Initialize logging
+    init_logging
+
+    if [ "$QUIET" = false ]; then
+        echo ""
+        echo -e "${GREEN}Smartbotic Update${NC}"
+        echo "================="
+    fi
+
+    log "Package dir: $PACKAGE_DIR"
+    log "Rolling: $ROLLING"
+    log "Backup: $BACKUP"
+
+    if [ "$DRY_RUN" = true ]; then
+        print_warn "DRY-RUN MODE - no changes will be made"
+    fi
+
+    # Create backup if requested
+    create_backup
+
+    # Collect dependencies from all packages
+    print_step "Checking packages..."
+    local pkg_count=0
+    for pkg in "$PACKAGE_DIR"/smartbotic-*.tar.gz; do
+        if [ -f "$pkg" ]; then
+            pkg_count=$((pkg_count + 1))
+            local tmpdir=$(mktemp -d)
+            tar -xzf "$pkg" -C "$tmpdir" 2>/dev/null
+            local pkg_root=$(find "$tmpdir" -maxdepth 1 -type d ! -path "$tmpdir" | head -1)
+            if [ -f "$pkg_root/DEPS" ]; then
+                collect_deps "$(cat "$pkg_root/DEPS")"
+            fi
+            rm -rf "$tmpdir"
+        fi
+    done
+    print_summary "Found $pkg_count packages"
+
+    install_deps
+
+    if [ "$ROLLING" = false ] && [ "$SKIP_RESTART" = false ]; then
+        # Stop all services in reverse order
+        print_step "Stopping services..."
+        for service in $(printf '%s\n' "${SERVICES[@]}" | tac); do
+            stop_service "$service"
+        done
+        print_summary "Services stopped"
+    fi
+
+    # Update each service
+    for service in "${SERVICES[@]}"; do
+        update_service "$service"
+    done
+
+    # Update WebUI
+    update_webui
+
+    # Reload systemd
+    print_step "Finalizing..."
+    run_cmd systemctl daemon-reload
+
+    if [ "$ROLLING" = false ] && [ "$SKIP_RESTART" = false ]; then
+        # Start all services
+        print_step "Starting services..."
+        for service in "${SERVICES[@]}"; do
+            start_service "$service"
+        done
+        print_summary "Services started"
+    fi
+
+    print_summary "Update complete"
+
+    if [ "$QUIET" = false ]; then
+        echo ""
+        echo -e "${GREEN}Done!${NC} Log: $UPDATE_LOG"
+        echo ""
+        echo "Verify:"
+        echo "  systemctl status smartbotic-database smartbotic-microbit"
+        echo "  journalctl -u smartbotic-microbit -f"
+        echo ""
+    fi
+
+    log "Update completed successfully"
+}
+
+main

+ 42 - 0
packaging/systemd/smartbotic-database.service

@@ -0,0 +1,42 @@
+[Unit]
+Description=Smartbotic Database Service
+Documentation=https://git.smartbotics.ai/fszontagh/smartbotic-database
+After=network.target
+
+[Service]
+Type=notify
+ExecStart=/opt/smartbotic/bin/smartbotic-database --config /etc/smartbotic/database.json
+Restart=on-failure
+RestartSec=5
+TimeoutStartSec=30
+TimeoutStopSec=30
+WatchdogSec=30
+
+# User/Group
+User=smartbotic
+Group=smartbotic
+
+# Environment
+EnvironmentFile=-/etc/smartbotic/smartbotic.env
+Environment=LOG_LEVEL=info
+
+# Working directory
+WorkingDirectory=/var/lib/smartbotic/database
+
+# Security hardening
+NoNewPrivileges=yes
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=yes
+MemoryDenyWriteExecute=true
+SystemCallFilter=@system-service
+
+# Writable paths
+ReadWritePaths=/var/lib/smartbotic/database /var/log/smartbotic
+
+# Resource limits
+LimitNOFILE=65536
+LimitNPROC=4096
+
+[Install]
+WantedBy=multi-user.target

+ 43 - 0
packaging/systemd/smartbotic-microbit.service

@@ -0,0 +1,43 @@
+[Unit]
+Description=Smartbotic MicroBit Service
+Documentation=https://git.smartbotics.ai/fszontagh/smartbotic-microbit
+After=network.target smartbotic-database.service
+Wants=smartbotic-database.service
+
+[Service]
+Type=notify
+ExecStart=/opt/smartbotic/bin/smartbotic-microbit --config /etc/smartbotic/microbit.json
+Restart=on-failure
+RestartSec=5
+TimeoutStartSec=30
+TimeoutStopSec=30
+WatchdogSec=30
+
+# User/Group
+User=smartbotic
+Group=smartbotic
+
+# Environment
+EnvironmentFile=-/etc/smartbotic/smartbotic.env
+Environment=LOG_LEVEL=info
+
+# Working directory
+WorkingDirectory=/var/lib/smartbotic/microbit
+
+# Security hardening
+NoNewPrivileges=yes
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=yes
+MemoryDenyWriteExecute=true
+SystemCallFilter=@system-service
+
+# Writable paths
+ReadWritePaths=/var/lib/smartbotic/microbit /var/log/smartbotic
+
+# Resource limits
+LimitNOFILE=65536
+LimitNPROC=4096
+
+[Install]
+WantedBy=multi-user.target