diff --git a/.gitignore b/.gitignore index ee4133c..662e079 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,6 @@ pishrink.sh # Temp file storage frontends/web/temp_files/ rpi/config.json + +# Pre-built venv tarball (39MB - too large for git) +rpi/stegasoo-venv-pi-arm64.tar.zst diff --git a/PLAN-4.1.4.md b/PLAN-4.1.4.md index c100cee..1bcf85d 100644 --- a/PLAN-4.1.4.md +++ b/PLAN-4.1.4.md @@ -1,17 +1,157 @@ # Stegasoo 4.1.4 Plan ## Build / Deploy -- [ ] Pre-built Python 3.12 venv tarball for Pi (skip 20+ min compile) +- [x] Pre-built Python 3.12 venv tarball for Pi (skip 20+ min compile) - see details below - [x] Fixed partition sizing in flash script (16GB rootfs for faster imaging) - [x] Rename `flash-pi.sh` → `flash-stock-img.sh` for clarity - [x] pip-audit integration in release validation +### Pi venv Tarball Approach +1. Flash fresh Pi image, let it fully build (20+ min compile) +2. Once running and working, SSH in and create optimized tarball: + ```bash + cd /opt/stegasoo + # Strip caches and tests (295MB → 208MB) + find venv/ -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null + find venv/ -type d -name 'tests' -exec rm -rf {} + 2>/dev/null + find venv/ -type d -name 'test' -exec rm -rf {} + 2>/dev/null + # Compress with zstd (208MB → 39MB) + tar -cf - venv/ | zstd -19 -T0 > /tmp/stegasoo-venv-pi-arm64.tar.zst + ``` +3. Pull tarball to host: `scp admin@pi:/tmp/stegasoo-venv-pi-arm64.tar.zst rpi/` +4. setup.sh auto-detects and extracts tarball if present in rpi/ +5. Re-flash and test fresh build with pre-built venv (should be <2 min vs 20+) + ## Features -- [ ] QR channel key sharing (needs UI thought - avoid crowding encode/decode pages) +- [x] QR channel key sharing (see detailed plan below) - [ ] Role-based permissions: admin / mod / user - [x] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.) - [ ] Better capacity estimates / pre-flight check before encode fails +--- + +## QR Channel Key Sharing - Implementation Plan + +### Current State +- ✅ **CLI**: `stegasoo channel qr` generates ASCII/PNG QR for server channel key +- ✅ **Web UI (about.html)**: Client-side QR generator exists - input key, generate/show QR, download PNG +- ✅ **Account page**: Shows saved channel keys with fingerprint, rename, delete +- ❌ No role restrictions on QR sharing +- ❌ No QR button for saved keys on account page +- ❌ No QR scanning to import keys + +### Design Decisions + +**UI Placement** (avoiding encode/decode page crowding): +- Keep QR generator in **about.html** (already exists, logical place for tools) +- Add QR button to **account.html** saved keys (small icon, doesn't crowd) +- Both should be admin-only + +**Role Restriction** (per user request): +- QR sharing = admin only (hide generator + saved key QR buttons from non-admins) +- Prerequisite: Need role-based permissions feature first +- Interim option: Just hide from non-admin users using existing `is_admin` flag + +### Implementation Steps + +#### Phase 1: Admin-only restriction (quick win) +1. **about.html**: Wrap QR generator section in `{% if is_admin %}` block +2. **Account route**: Pass `is_admin` to template (if not already) +3. **account.html**: Add small QR icon button to saved keys row (admin only) + - Opens modal with QR canvas (reuse qrcode.js pattern from about.html) + - Download PNG button in modal + +#### Phase 2: QR Import (optional enhancement) +1. Add "Import via QR" button to account.html key-add section +2. Use device camera or file upload to scan QR +3. Decode and populate channel_key input field +4. Requires `pyzbar` on server OR client-side JS library like `jsQR` + +### Files to Modify + +``` +frontends/web/app.py + - about() route: Add missing vars: is_admin, channel_configured, + channel_fingerprint, channel_source (BUG: currently not passed!) + - account() route: ✅ Already passes is_admin + +frontends/web/templates/about.html + - Wrap channel key QR section in {% if is_admin %} + +frontends/web/templates/account.html + - Add QR button to saved keys (admin only) + - Add QR modal (copy pattern from about.html) + - Include qrcode.min.js CDN script +``` + +### Bug Found During Research +The about.html template uses `channel_configured`, `channel_fingerprint`, +`channel_source` but the route doesn't pass them - always shows "public mode". +Fix this while implementing QR admin restriction. + +### Exact Code Changes + +**app.py - Fix about() route (around line 1564):** +```python +@app.route("/about") +def about(): + from stegasoo.channel import get_channel_status + channel_status = get_channel_status() + + # Check if user is admin (for QR sharing) + current_user = get_current_user() + is_admin = current_user.is_admin if current_user else False + + return render_template( + "about.html", + has_argon2=has_argon2(), + has_qrcode_read=HAS_QRCODE_READ, + # Channel info (bugfix) + channel_configured=channel_status["configured"], + channel_fingerprint=channel_status.get("fingerprint"), + channel_source=channel_status.get("source"), + # Admin check for QR sharing + is_admin=is_admin, + ) +``` + +### Template Changes Preview + +**account.html - Add to saved key row:** +```html +{% if is_admin %} + +{% endif %} +``` + +**about.html - Wrap existing section:** +```html +{% if is_admin %} + +
+ ...existing QR generator... +
+{% endif %} +``` + +### Testing Checklist (Phase 1 Implemented) +- [ ] Non-admin users cannot see QR generator in about.html +- [ ] Non-admin users cannot see QR buttons on account page +- [ ] Admin users can generate QR for any saved key +- [ ] QR downloads work correctly +- [ ] QR scans correctly with phone camera + +### Implementation Status +**Phase 1: COMPLETE** - Admin-only QR sharing implemented: +- `app.py`: Fixed about() route to pass channel status + is_admin +- `about.html`: QR generator wrapped in `{% if is_admin %}` with Admin badge +- `account.html`: QR button added to saved keys (admin only), modal + JS for generation/download + +--- + ## Security - [ ] Optional encryption for temp file storage (paranoid mode, config toggle) diff --git a/frontends/web/app.py b/frontends/web/app.py index 48700f1..1730d4c 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -1561,7 +1561,25 @@ def decode_download(file_id): @app.route("/about") def about(): - return render_template("about.html", has_argon2=has_argon2(), has_qrcode_read=HAS_QRCODE_READ) + from stegasoo.channel import get_channel_status + + channel_status = get_channel_status() + + # Check if user is admin (for QR sharing) + current_user = get_current_user() + is_admin = current_user.is_admin if current_user else False + + return render_template( + "about.html", + has_argon2=has_argon2(), + has_qrcode_read=HAS_QRCODE_READ, + # Channel info (bugfix - was not being passed) + channel_configured=channel_status["configured"], + channel_fingerprint=channel_status.get("fingerprint"), + channel_source=channel_status.get("source"), + # Admin check for QR sharing + is_admin=is_admin, + ) # ============================================================================ diff --git a/frontends/web/templates/about.html b/frontends/web/templates/about.html index 5420405..76fdef3 100644 --- a/frontends/web/templates/about.html +++ b/frontends/web/templates/about.html @@ -331,10 +331,12 @@ {% endif %} - + + {% if is_admin %}
Share Channel Key via QR + Admin

Generate a QR code to share a channel key with others.

@@ -366,6 +368,7 @@
+ {% endif %} diff --git a/frontends/web/templates/account.html b/frontends/web/templates/account.html index f958455..881fdbd 100644 --- a/frontends/web/templates/account.html +++ b/frontends/web/templates/account.html @@ -140,6 +140,13 @@ {% endif %}
+ {% if is_admin %} + + {% endif %}
+ +{% if is_admin %} + + +{% endif %} {% endblock %} {% block scripts %} +{% if is_admin %} + +{% endif %} {% endblock %} diff --git a/rpi/BUILD_IMAGE.md b/rpi/BUILD_IMAGE.md index 5605261..7c7b89b 100644 --- a/rpi/BUILD_IMAGE.md +++ b/rpi/BUILD_IMAGE.md @@ -26,8 +26,8 @@ ssh admin@stegasoo.local # Take ownership of /opt (for pyenv, jpegio builds) sudo chown admin:admin /opt -# Install git (not included in Lite image) -sudo apt-get update && sudo apt-get install -y git +# Install git and zstd (not included in Lite image) +sudo apt-get update && sudo apt-get install -y git zstd jq ``` ## Step 4: Clone & Run Setup @@ -39,7 +39,22 @@ cd stegasoo ./rpi/setup.sh ``` -This takes ~15-20 minutes and installs: +### Fast Build Option (with pre-built venv) + +If you have `stegasoo-venv-pi-arm64.tar.zst` from a previous build: + +```bash +cd /opt +git clone -b 4.1 https://github.com/adlee-was-taken/stegasoo.git stegasoo + +# Copy pre-built venv (from your host machine) +# On host: scp rpi/stegasoo-venv-pi-arm64.tar.zst admin@stegasoo.local:/opt/stegasoo/rpi/ + +cd stegasoo +./rpi/setup.sh # Detects tarball, extracts instead of compiling (~2 min vs 20+) +``` + +**Standard build** takes ~15-20 minutes and installs: - Python 3.12 via pyenv - jpegio (patched for ARM) - Stegasoo with web UI diff --git a/rpi/flash-image.sh b/rpi/flash-image.sh index fbfdea5..1dffea4 100755 --- a/rpi/flash-image.sh +++ b/rpi/flash-image.sh @@ -8,9 +8,14 @@ # Supports: .img, .img.zst, .img.xz, .img.gz, .img.zst.zip (GitHub release format) # If device is specified, skips auto-detection (useful for NVMe/large drives) # +# Optional: Place config.json in same directory for headless WiFi setup +# set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG_FILE="$SCRIPT_DIR/config.json" + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -18,6 +23,28 @@ BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' +# Load config if present (optional - for headless WiFi setup) +HAS_CONFIG=false +if [ -f "$CONFIG_FILE" ] && command -v jq &> /dev/null; then + WIFI_SSID=$(jq -r '.wifiSSID // empty' "$CONFIG_FILE") + WIFI_PASS=$(jq -r '.wifiPassword // empty' "$CONFIG_FILE") + WIFI_COUNTRY=$(jq -r '.wifiCountry // "US"' "$CONFIG_FILE") + PI_HOSTNAME=$(jq -r '.hostname // empty' "$CONFIG_FILE") + if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PASS" ]; then + HAS_CONFIG=true + echo -e "${GREEN}Found config.json - will configure WiFi after flash${NC}" + echo -e " WiFi: ${YELLOW}$WIFI_SSID${NC}" + if [ -n "$PI_HOSTNAME" ]; then + echo -e " Hostname: ${YELLOW}$PI_HOSTNAME${NC}" + fi + echo "" + fi +elif [ -f "$CONFIG_FILE" ]; then + echo -e "${YELLOW}Note: config.json found but jq not installed (apt install jq)${NC}" + echo -e "${YELLOW} WiFi will need to be configured manually after boot${NC}" + echo "" +fi + # Check for required tools for cmd in dd lsblk; do if ! command -v $cmd &> /dev/null; then @@ -222,6 +249,17 @@ if [ -n "$MOUNTED" ]; then done fi +# Ask about wiping +echo +read -p "Wipe partition table first? (recommended if having issues) [y/N] " wipe_confirm +if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then + echo "Wiping partition table..." + sudo wipefs -a "$SELECTED" + sudo dd if=/dev/zero of="$SELECTED" bs=1M count=10 status=none + sync + echo " Wiped clean" +fi + # Final confirmation echo -e "${RED}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${RED}║ WARNING: ALL DATA ON THIS DEVICE WILL BE DESTROYED! ║${NC}" @@ -284,6 +322,109 @@ echo "" echo -e "${GREEN}Syncing...${NC}" sync +# Inject WiFi config if config.json was loaded +if [ "$HAS_CONFIG" = true ]; then + echo "" + echo -e "${GREEN}Configuring WiFi from config.json...${NC}" + + # Wait for partitions to appear + sleep 2 + partprobe "$SELECTED" 2>/dev/null || true + sleep 1 + + # Determine boot partition + if [[ "$SELECTED" == *"nvme"* ]] || [[ "$SELECTED" == *"mmcblk"* ]]; then + BOOT_PART="${SELECTED}p1" + else + BOOT_PART="${SELECTED}1" + fi + + if [ -b "$BOOT_PART" ]; then + MOUNT_DIR=$(mktemp -d) + if mount "$BOOT_PART" "$MOUNT_DIR" 2>/dev/null; then + # Create firstrun.sh for WiFi setup + cat > "$MOUNT_DIR/firstrun.sh" << 'EOFSCRIPT' +#!/bin/bash +set +e + +# Set hostname if provided +if [ -n "PLACEHOLDER_HOSTNAME" ] && [ "PLACEHOLDER_HOSTNAME" != "" ]; then + CURRENT_HOSTNAME=$(cat /etc/hostname | tr -d " \t\n\r") + if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then + /usr/lib/raspberrypi-sys-mods/imager_custom set_hostname PLACEHOLDER_HOSTNAME + else + echo PLACEHOLDER_HOSTNAME >/etc/hostname + sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\tPLACEHOLDER_HOSTNAME/g" /etc/hosts + fi +fi + +# Configure WiFi +if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then + /usr/lib/raspberrypi-sys-mods/imager_custom set_wlan 'PLACEHOLDER_SSID' 'PLACEHOLDER_WIFIPASS' 'PLACEHOLDER_COUNTRY' +else + # NetworkManager method (Trixie) + cat >/etc/NetworkManager/system-connections/preconfigured.nmconnection <<'NMEOF' +[connection] +id=preconfigured +type=wifi +autoconnect=true + +[wifi] +mode=infrastructure +ssid=PLACEHOLDER_SSID + +[wifi-security] +auth-alg=open +key-mgmt=wpa-psk +psk=PLACEHOLDER_WIFIPASS + +[ipv4] +method=auto + +[ipv6] +method=auto +NMEOF + chmod 600 /etc/NetworkManager/system-connections/preconfigured.nmconnection + rfkill unblock wifi +fi + +# Cleanup +rm -f /boot/firstrun.sh +rm -f /boot/firmware/firstrun.sh +sed -i 's| systemd.run.*||g' /boot/cmdline.txt 2>/dev/null +sed -i 's| systemd.run.*||g' /boot/firmware/cmdline.txt 2>/dev/null +exit 0 +EOFSCRIPT + + # Replace placeholders + sed -i "s/PLACEHOLDER_SSID/$WIFI_SSID/g" "$MOUNT_DIR/firstrun.sh" + sed -i "s/PLACEHOLDER_WIFIPASS/$WIFI_PASS/g" "$MOUNT_DIR/firstrun.sh" + sed -i "s/PLACEHOLDER_COUNTRY/$WIFI_COUNTRY/g" "$MOUNT_DIR/firstrun.sh" + if [ -n "$PI_HOSTNAME" ]; then + sed -i "s/PLACEHOLDER_HOSTNAME/$PI_HOSTNAME/g" "$MOUNT_DIR/firstrun.sh" + else + sed -i "s/PLACEHOLDER_HOSTNAME//g" "$MOUNT_DIR/firstrun.sh" + fi + chmod +x "$MOUNT_DIR/firstrun.sh" + + # Update cmdline.txt to run firstrun.sh + CMDLINE="$MOUNT_DIR/cmdline.txt" + if [ -f "$CMDLINE" ]; then + CURRENT=$(cat "$CMDLINE" | tr -d '\n' | sed 's| systemd.run.*||g') + echo "$CURRENT systemd.run=/boot/firmware/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target" > "$CMDLINE" + fi + + umount "$MOUNT_DIR" + echo -e " ${GREEN}✓${NC} WiFi configured for: $WIFI_SSID" + else + echo -e " ${YELLOW}⚠${NC} Could not mount boot partition" + fi + rmdir "$MOUNT_DIR" 2>/dev/null || true + else + echo -e " ${YELLOW}⚠${NC} Boot partition not found" + fi +fi + echo "" echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Flash Complete! ║${NC}" @@ -291,5 +432,11 @@ echo -e "${GREEN}╚════════════════════ echo "" echo -e "You can now remove the SD card and boot your Raspberry Pi." echo "" -echo -e "${YELLOW}Tip:${NC} On first boot, SSH in and the setup wizard will run automatically." +if [ "$HAS_CONFIG" = true ]; then + echo -e "${GREEN}WiFi pre-configured${NC} - Pi will connect to $WIFI_SSID on boot" + echo -e "SSH: ${YELLOW}ssh admin@${PI_HOSTNAME:-stegasoo}.local${NC} (password: stegasoo)" +else + echo -e "${YELLOW}Tip:${NC} On first boot, the setup wizard will help configure WiFi." + echo -e "${YELLOW}Tip:${NC} Or place config.json in rpi/ for headless setup next time." +fi echo "" diff --git a/rpi/pull-image.sh b/rpi/pull-image.sh index 0eaa926..d22e326 100755 --- a/rpi/pull-image.sh +++ b/rpi/pull-image.sh @@ -168,12 +168,41 @@ fi DEV_SIZE=$(blockdev --getsize64 "$SELECTED") echo "" -echo -e "${GREEN}[1/3]${NC} Copying image from $SELECTED..." +echo -e "${GREEN}[1/4]${NC} Copying image from $SELECTED..." dd if="$SELECTED" bs=4M status=none | pv -s "$DEV_SIZE" > "$IMG_FILE" sync echo "" -echo -e "${GREEN}[2/3]${NC} Shrinking image..." +echo -e "${GREEN}[2/4]${NC} Re-enabling auto-expand for distribution..." +# Mount the image and restore auto-expand service (may have been disabled during build) +LOOP_DEV=$(losetup -f --show -P "$IMG_FILE") +if [ -n "$LOOP_DEV" ]; then + TEMP_MOUNT=$(mktemp -d) + if mount "${LOOP_DEV}p2" "$TEMP_MOUNT" 2>/dev/null; then + # Re-enable the resize service if the service file exists + SERVICE_FILE="$TEMP_MOUNT/lib/systemd/system/rpi-resizerootfs.service" + SERVICE_LINK="$TEMP_MOUNT/etc/systemd/system/multi-user.target.wants/rpi-resizerootfs.service" + if [ -f "$SERVICE_FILE" ] && [ ! -L "$SERVICE_LINK" ]; then + mkdir -p "$(dirname "$SERVICE_LINK")" + ln -sf /lib/systemd/system/rpi-resizerootfs.service "$SERVICE_LINK" + echo -e " ${GREEN}✓${NC} Auto-expand service re-enabled" + elif [ -L "$SERVICE_LINK" ]; then + echo -e " ${GREEN}✓${NC} Auto-expand already enabled" + else + echo -e " ${YELLOW}⚠${NC} Could not find resize service file" + fi + umount "$TEMP_MOUNT" + else + echo -e " ${YELLOW}⚠${NC} Could not mount rootfs, skipping auto-expand fix" + fi + rmdir "$TEMP_MOUNT" 2>/dev/null || true + losetup -d "$LOOP_DEV" +else + echo -e " ${YELLOW}⚠${NC} Could not create loop device, skipping auto-expand fix" +fi + +echo "" +echo -e "${GREEN}[3/4]${NC} Shrinking image..." if command -v pishrink.sh &> /dev/null; then pishrink.sh "$IMG_FILE" elif [ -f "./pishrink.sh" ]; then @@ -187,11 +216,11 @@ fi echo "" if [ "$SKIP_COMPRESS" = true ]; then - echo -e "${GREEN}[3/3]${NC} Skipping compression (.img output)" + echo -e "${GREEN}[4/4]${NC} Skipping compression (.img output)" FINAL_SIZE=$(du -h "$IMG_FILE" | awk '{print $1}') OUTPUT="$IMG_FILE" else - echo -e "${GREEN}[3/3]${NC} Compressing with zstd..." + echo -e "${GREEN}[4/4]${NC} Compressing with zstd..." pv "$IMG_FILE" | zstd -19 -T0 -q > "$OUTPUT" rm -f "$IMG_FILE" FINAL_SIZE=$(du -h "$OUTPUT" | awk '{print $1}') diff --git a/rpi/setup.sh b/rpi/setup.sh index 454def7..d98601f 100755 --- a/rpi/setup.sh +++ b/rpi/setup.sh @@ -135,6 +135,7 @@ sudo apt-get install -y \ build-essential \ git \ curl \ + zstd \ libssl-dev \ zlib1g-dev \ libbz2-dev \ @@ -218,50 +219,84 @@ else cd "$INSTALL_DIR" fi -echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..." +# Check for pre-built venv tarball (skips 20+ min compile time) +PREBUILT_VENV="$INSTALL_DIR/rpi/stegasoo-venv-pi-arm64.tar.zst" +PREBUILT_VENV_URL="${PREBUILT_VENV_URL:-}" # Optional: URL to download from -# Create venv with pyenv Python (not system Python) -# Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping) -PYENV_PYTHON=$(pyenv which python) -echo " Using Python: $PYENV_PYTHON" -if [ ! -d "venv" ]; then - "$PYENV_PYTHON" -m venv venv -fi -source venv/bin/activate +if [ -f "$PREBUILT_VENV" ] || [ -n "$PREBUILT_VENV_URL" ]; then + echo -e "${GREEN}[6/8]${NC} Installing pre-built Python environment..." -# Verify we're using the right Python -VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2) -echo " venv Python: $VENV_PY" + # Download if URL provided and local file doesn't exist + if [ ! -f "$PREBUILT_VENV" ] && [ -n "$PREBUILT_VENV_URL" ]; then + echo " Downloading pre-built venv..." + curl -L -o "$PREBUILT_VENV" "$PREBUILT_VENV_URL" + fi -echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..." + # Extract pre-built venv (zstd compressed) + echo " Extracting pre-built venv (this is much faster!)..." + zstd -d "$PREBUILT_VENV" --stdout | tar -xf - -C "$INSTALL_DIR" -# Clone jpegio -JPEGIO_DIR="/tmp/jpegio-build" -rm -rf "$JPEGIO_DIR" -git clone "$JPEGIO_REPO" "$JPEGIO_DIR" + # Activate and verify + source venv/bin/activate + VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2) + echo -e " ${GREEN}✓${NC} venv Python: $VENV_PY" -# Apply ARM64 patch -if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then - bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR" + # Install stegasoo package in editable mode (quick, no compile) + echo -e "${GREEN}[7/8]${NC} Installing Stegasoo package..." + pip install -e "." --quiet + + # Adjust step numbers for rest of script + STEP_OFFSET=-4 else - echo " Applying inline ARM64 patch..." - sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py" + echo -e "${GREEN}[6/12]${NC} Creating Python virtual environment..." + echo -e " ${YELLOW}Note: No pre-built venv found. Building from source (20+ min)${NC}" + echo -e " ${YELLOW}To speed up future installs, add stegasoo-venv-pi-arm64.tar.gz to rpi/${NC}" + + # Create venv with pyenv Python (not system Python) + # Use pyenv which to get actual path (handles 3.12 -> 3.12.12 mapping) + PYENV_PYTHON=$(pyenv which python) + echo " Using Python: $PYENV_PYTHON" + if [ ! -d "venv" ]; then + "$PYENV_PYTHON" -m venv venv + fi + source venv/bin/activate + + # Verify we're using the right Python + VENV_PY=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2) + echo " venv Python: $VENV_PY" + + echo -e "${GREEN}[7/12]${NC} Building jpegio for ARM..." + + # Clone jpegio + JPEGIO_DIR="/tmp/jpegio-build" + rm -rf "$JPEGIO_DIR" + git clone "$JPEGIO_REPO" "$JPEGIO_DIR" + + # Apply ARM64 patch + if [ -f "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" ]; then + bash "$INSTALL_DIR/rpi/patches/jpegio/apply-patch.sh" "$JPEGIO_DIR" + else + echo " Applying inline ARM64 patch..." + sed -i "s/cargs.append('-m64')/pass # ARM64 fix/g" "$JPEGIO_DIR/setup.py" + fi + + cd "$JPEGIO_DIR" + + # Build jpegio into venv + pip install --upgrade pip setuptools wheel cython numpy + pip install . + + cd "$INSTALL_DIR" + rm -rf "$JPEGIO_DIR" + + echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..." + + # Install dependencies (jpegio already in venv, won't re-download) + pip install -e ".[web]" + + STEP_OFFSET=0 fi -cd "$JPEGIO_DIR" - -# Build jpegio into venv -pip install --upgrade pip setuptools wheel cython numpy -pip install . - -cd "$INSTALL_DIR" -rm -rf "$JPEGIO_DIR" - -echo -e "${GREEN}[8/12]${NC} Installing Stegasoo..." - -# Install dependencies (jpegio already in venv, won't re-download) -pip install -e ".[web]" - echo -e "${GREEN}[9/12]${NC} Creating systemd service..." # Create systemd service file