secubox-openwrt/package/secubox/secubox-app-streamlit/files/usr/sbin/streamlitctl
CyberMind-FR 36e61cead8 fix(streamlit): Fix emancipate to use app name not instance name
The emancipate function was checking for app folder existence using
instance name (e.g., "pix") instead of the actual app name
(e.g., "bazi_calculator"). Now properly resolves app from UCI config.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 08:59:18 +01:00

1786 lines
47 KiB
Bash

#!/bin/sh
# SecuBox Streamlit Platform Controller
# Copyright (C) 2025 CyberMind.fr
#
# Manages Streamlit in LXC container
# Supports multi-instance with folder-based apps and Gitea integration
# Source OpenWrt UCI functions
. /lib/functions.sh
CONFIG="streamlit"
LXC_NAME="streamlit"
# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DEFAULT_APP="/usr/share/streamlit/hello.py"
# Logging
log_info() { echo "[INFO] $*"; logger -t streamlit "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t streamlit -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t streamlit -p err "$*"; }
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
# Helpers
require_root() {
[ "$(id -u)" -eq 0 ] || {
log_error "This command requires root privileges"
exit 1
}
}
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
has_git() { command -v git >/dev/null 2>&1; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration
load_config() {
http_port="$(uci_get main.http_port)" || http_port="8501"
http_host="$(uci_get main.http_host)" || http_host="0.0.0.0"
data_path="$(uci_get main.data_path)" || data_path="/srv/streamlit"
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
active_app="$(uci_get main.active_app)" || active_app="hello"
# Server settings
headless="$(uci_get server.headless)" || headless="true"
gather_stats="$(uci_get server.browser_gather_usage_stats)" || gather_stats="false"
theme_base="$(uci_get server.theme_base)" || theme_base="dark"
theme_primary="$(uci_get server.theme_primary_color)" || theme_primary="#0ff"
# Gitea config
gitea_enabled="$(uci_get gitea.enabled)" || gitea_enabled="0"
gitea_url="$(uci_get gitea.url)" || gitea_url="http://192.168.255.1:3000"
gitea_user="$(uci_get gitea.user)" || gitea_user="admin"
gitea_token="$(uci_get gitea.token)" || gitea_token=""
ensure_dir "$data_path"
ensure_dir "$data_path/apps"
ensure_dir "$data_path/logs"
APPS_PATH="$data_path/apps"
}
# Usage
usage() {
cat <<EOF
SecuBox Streamlit Platform Controller (Multi-Instance)
Usage: $(basename $0) <command> [options]
Container Commands:
install Download and setup LXC container
uninstall Remove container (preserves apps)
update Update Streamlit in container
status Show service status
logs [app] Show logs
App Management (folder-based):
app list List all apps
app create <name> Create new app folder
app delete <name> Delete app folder
app deploy <name> <path> Deploy app from path/archive
Instance Management:
instance list List configured instances
instance add <app> <port> Add instance for app on port
instance remove <name> Remove instance
instance start <name> Start single instance
instance stop <name> Stop single instance
Gitea Integration:
gitea setup Configure git credentials
gitea clone <name> <repo> Clone app from Gitea repo
gitea pull <name> Pull latest from Gitea
gitea push <name> Create Gitea repo and push app content
gitea init-all Initialize Gitea repos for all existing apps
Exposure:
emancipate <name> [domain] KISS ULTIME MODE - Full exposure workflow:
1. DNS A record (Gandi/OVH)
2. Vortex DNS mesh publication
3. HAProxy vhost with SSL
4. ACME certificate
5. Zero-downtime reload
Service Commands:
service-run Start all instances (for init)
service-stop Stop all instances
Utility:
shell Open shell in container
exec <cmd> Execute command in container
App Folder Structure:
/srv/streamlit/apps/<appname>/
app.py Main Streamlit app (or <appname>.py)
requirements.txt Python dependencies
.streamlit/ Streamlit config (optional)
... Other files
Examples:
# Create local app
streamlitctl app create myapp
streamlitctl instance add myapp 8502
/etc/init.d/streamlit restart
# Deploy from Gitea (complete workflow)
uci set streamlit.gitea.enabled=1
uci set streamlit.gitea.url='http://192.168.255.1:3000'
uci set streamlit.gitea.user='admin'
uci set streamlit.gitea.token='your-token'
uci commit streamlit
streamlitctl gitea setup
streamlitctl gitea clone yijing CyberMood/yijing-oracle
streamlitctl instance add yijing 8505
/etc/init.d/streamlit restart
# Access at: http://<device-ip>:8505
# Update app from Gitea
streamlitctl gitea pull yijing
/etc/init.d/streamlit restart
EOF
}
# Check prerequisites
lxc_check_prereqs() {
if ! has_lxc; then
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
return 1
fi
return 0
}
# Create Python LXC rootfs from Alpine
lxc_create_rootfs() {
local rootfs="$LXC_ROOTFS"
local arch=$(uname -m)
log_info "Creating Alpine rootfs for Streamlit..."
ensure_dir "$rootfs"
local alpine_version="3.21"
case "$arch" in
x86_64) alpine_arch="x86_64" ;;
aarch64) alpine_arch="aarch64" ;;
armv7l) alpine_arch="armv7" ;;
*) log_error "Unsupported architecture: $arch"; return 1 ;;
esac
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz"
local tmpfile="/tmp/alpine-rootfs.tar.gz"
log_info "Downloading Alpine ${alpine_version} rootfs..."
wget -q -O "$tmpfile" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
tar -xzf "$tmpfile" -C "$rootfs" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$tmpfile"
cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \
echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf"
log_info "Rootfs created successfully"
return 0
}
# Create startup script for multi-instance folder-based apps
create_startup_script() {
load_config
local start_script="$LXC_ROOTFS/opt/start-streamlit.sh"
cat > "$start_script" << 'STARTUP'
#!/bin/sh
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH
export HOME=/root
APPS_BASE="/srv/apps"
PIDS_DIR="/var/run/streamlit"
LOG_DIR="/var/log/streamlit"
INSTANCES_CONF="/srv/apps/instances.conf"
mkdir -p "$PIDS_DIR" "$LOG_DIR"
# Install Python and Streamlit on first run
if [ ! -f /opt/.installed ]; then
echo "Installing Python 3.12 and dependencies..."
apk update
apk add --no-cache python3 py3-pip git procps poppler-utils
echo "Installing Streamlit..."
pip3 install --break-system-packages streamlit 2>/dev/null || \
pip3 install streamlit 2>/dev/null
touch /opt/.installed
echo "Installation complete!"
fi
# Create default hello app if missing
if [ ! -d "$APPS_BASE/hello" ]; then
mkdir -p "$APPS_BASE/hello"
cat > "$APPS_BASE/hello/app.py" << 'HELLO'
import streamlit as st
st.set_page_config(page_title="SecuBox Streamlit", page_icon="⚡", layout="wide")
st.title("⚡ SECUBOX STREAMLIT ⚡")
st.markdown("### Neural Data App Platform")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Status", "ONLINE", delta="Active")
with col2:
st.metric("Apps", "Ready", delta="+1")
with col3:
st.metric("Platform", "SecuBox")
st.info("Deploy your Streamlit apps via LuCI dashboard or CLI")
HELLO
fi
# Function to find main app file in folder
find_app_file() {
local app_dir="$1"
local app_name=$(basename "$app_dir")
# Priority: app.py > main.py > <appname>.py > first .py file
for candidate in "$app_dir/app.py" "$app_dir/main.py" "$app_dir/${app_name}.py"; do
[ -f "$candidate" ] && { echo "$candidate"; return 0; }
done
# Fallback to first .py file
local first_py=$(ls -1 "$app_dir"/*.py 2>/dev/null | head -1)
[ -n "$first_py" ] && { echo "$first_py"; return 0; }
return 1
}
# Function to install requirements for an app
install_requirements() {
local app_dir="$1"
local app_name=$(basename "$app_dir")
# Find requirements file: requirements.txt first, then any requirements*.txt
local req_file=""
if [ -f "$app_dir/requirements.txt" ]; then
req_file="$app_dir/requirements.txt"
else
req_file=$(ls -1 "$app_dir"/requirements*.txt 2>/dev/null | head -1)
fi
if [ -n "$req_file" ] && [ -f "$req_file" ]; then
REQ_HASH=$(md5sum "$req_file" 2>/dev/null | cut -d' ' -f1)
REQ_MARKER="/opt/.req_${app_name}_${REQ_HASH}"
if [ ! -f "$REQ_MARKER" ]; then
echo "Installing requirements from $(basename $req_file) for $app_name..."
pip3 install --break-system-packages -r "$req_file" 2>/dev/null || \
pip3 install -r "$req_file" 2>/dev/null || true
touch "$REQ_MARKER"
fi
fi
}
# Function to start a single instance
start_instance() {
local app_name="$1"
local port="$2"
local app_dir="$APPS_BASE/$app_name"
local app_file=""
local work_dir=""
if [ -d "$app_dir" ]; then
# Folder-based app (ZIP upload, Gitea clone, or created via CLI)
app_file=$(find_app_file "$app_dir")
if [ -z "$app_file" ]; then
echo "No Python app file found in $app_dir"
return 1
fi
install_requirements "$app_dir"
work_dir="$app_dir"
elif [ -f "$APPS_BASE/${app_name}.py" ]; then
# Top-level single .py file (direct upload)
app_file="$APPS_BASE/${app_name}.py"
work_dir="$APPS_BASE"
else
echo "App not found: $app_name (no folder or .py file)"
return 1
fi
echo "Starting instance: $app_name on port $port (file: $(basename $app_file))"
# Change to app/work directory so relative imports work
cd "$work_dir"
nohup streamlit run "$(basename $app_file)" \
--server.address="0.0.0.0" \
--server.port="$port" \
--server.headless=true \
--browser.gatherUsageStats=false \
--theme.base="${STREAMLIT_THEME_BASE:-dark}" \
--theme.primaryColor="${STREAMLIT_THEME_PRIMARY:-#0ff}" \
> "$LOG_DIR/${app_name}.log" 2>&1 &
echo $! > "$PIDS_DIR/${app_name}.pid"
echo "Instance '$app_name' started (PID: $!)"
}
# Function to stop an instance
stop_instance() {
local app_name="$1"
local pidfile="$PIDS_DIR/${app_name}.pid"
if [ -f "$pidfile" ]; then
local pid=$(cat "$pidfile")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
echo "Stopped instance '$app_name' (PID: $pid)"
fi
rm -f "$pidfile"
fi
}
# Read instances from config file (format: appname:port)
if [ -f "$INSTANCES_CONF" ]; then
echo "Loading instances from config..."
while IFS=: read -r app_name port; do
[ -n "$app_name" ] && [ -n "$port" ] && start_instance "$app_name" "$port"
done < "$INSTANCES_CONF"
else
# Fallback: single instance mode
if [ -n "$STREAMLIT_APP" ] && [ -n "$STREAMLIT_PORT" ]; then
start_instance "$STREAMLIT_APP" "$STREAMLIT_PORT"
else
# Default: start hello on 8501
start_instance "hello" "8501"
fi
fi
# Keep container running
echo ""
echo "Streamlit multi-instance manager running."
echo "Instances:"
ls -1 "$PIDS_DIR"/*.pid 2>/dev/null | while read f; do
name=$(basename "$f" .pid)
pid=$(cat "$f")
echo " - $name (PID: $pid)"
done
# Monitor loop
while true; do
sleep 30
if ! pgrep -f "streamlit" >/dev/null; then
echo "No streamlit processes running, exiting..."
exit 1
fi
done
STARTUP
chmod +x "$start_script"
}
# Generate instances.conf from UCI
generate_instances_conf() {
load_config
local conf_file="$data_path/apps/instances.conf"
> "$conf_file"
# Iterate over instance sections
_add_instance_to_conf() {
local section="$1"
local inst_enabled inst_app inst_port
config_get inst_enabled "$section" enabled "0"
config_get inst_app "$section" app ""
config_get inst_port "$section" port ""
if [ "$inst_enabled" = "1" ] && [ -n "$inst_app" ] && [ -n "$inst_port" ]; then
echo "${inst_app}:${inst_port}" >> "$conf_file"
fi
}
config_load "$CONFIG"
config_foreach _add_instance_to_conf instance
# If no instances, add default
if [ ! -s "$conf_file" ]; then
echo "hello:8501" > "$conf_file"
fi
log_debug "Generated instances.conf: $(cat $conf_file | tr '\n' ' ')"
}
# Create LXC config
lxc_create_config() {
load_config
ensure_dir "$(dirname "$LXC_CONFIG")"
local mem_bytes
case "$memory_limit" in
*G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;;
*M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;;
*K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;;
*) mem_bytes="$memory_limit" ;;
esac
cat > "$LXC_CONFIG" << EOF
# Streamlit Platform LXC Configuration (Multi-Instance)
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $(uname -m)
# Network: use host network
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro
lxc.mount.entry = $APPS_PATH srv/apps none bind,create=dir 0 0
lxc.mount.entry = $data_path/logs srv/logs none bind,create=dir 0 0
lxc.mount.entry = /tmp/secubox tmp/secubox none bind,create=dir 0 0
# Environment
lxc.environment = STREAMLIT_THEME_BASE=$theme_base
lxc.environment = STREAMLIT_THEME_PRIMARY=$theme_primary
# Terminal
lxc.tty.max = 0
lxc.pty.max = 256
# Standard character devices
lxc.cgroup2.devices.allow = c 1:* rwm
lxc.cgroup2.devices.allow = c 5:* rwm
lxc.cgroup2.devices.allow = c 136:* rwm
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
# Resource limits
lxc.cgroup2.memory.max = $mem_bytes
# Init command
lxc.init.cmd = /opt/start-streamlit.sh
EOF
log_info "LXC config created"
}
# Container control
lxc_running() {
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}
lxc_exists() {
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}
lxc_stop() {
if lxc_running; then
log_info "Stopping Streamlit container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_run() {
load_config
lxc_stop
if ! lxc_exists; then
log_error "Container not installed. Run: streamlitctl install"
return 1
fi
lxc_create_config
create_startup_script
generate_instances_conf
log_info "Starting Streamlit container..."
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_exec() {
if ! lxc_running; then
log_error "Container not running. Start with: /etc/init.d/streamlit start"
return 1
fi
lxc-attach -n "$LXC_NAME" -- env PATH=/usr/local/bin:/usr/bin:/bin "$@"
}
# App management commands
cmd_app_list() {
load_config
echo "Streamlit Apps:"
echo "---------------"
if [ -d "$APPS_PATH" ]; then
for app_dir in "$APPS_PATH"/*/; do
[ -d "$app_dir" ] || continue
local name=$(basename "$app_dir")
# Find main file
local main_file=""
for candidate in "$app_dir/app.py" "$app_dir/main.py" "$app_dir/${name}.py"; do
[ -f "$candidate" ] && { main_file=$(basename "$candidate"); break; }
done
[ -z "$main_file" ] && main_file=$(ls -1 "$app_dir"/*.py 2>/dev/null | head -1 | xargs basename 2>/dev/null)
# Check for requirements (requirements.txt or any requirements*.txt)
local has_req="no"
[ -f "$app_dir/requirements.txt" ] && has_req="yes"
[ "$has_req" = "no" ] && ls -1 "$app_dir"/requirements*.txt >/dev/null 2>&1 && has_req="yes"
# Check for git repo
local has_git="no"
[ -d "$app_dir/.git" ] && has_git="yes"
printf " %-20s main:%-15s req:%-3s git:%-3s\n" "$name" "${main_file:-none}" "$has_req" "$has_git"
done
fi
# Also list legacy single-file apps
for app_file in "$APPS_PATH"/*.py; do
[ -f "$app_file" ] || continue
local name=$(basename "$app_file" .py)
# Skip if there's a folder with same name
[ -d "$APPS_PATH/$name" ] && continue
printf " %-20s (legacy single-file)\n" "$name"
done
}
cmd_app_create() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "App name required"; return 1; }
# Validate name
echo "$name" | grep -qE '^[a-z][a-z0-9_-]*$' || {
log_error "Invalid app name. Use lowercase, numbers, underscore, hyphen."
return 1
}
local app_dir="$APPS_PATH/$name"
if [ -d "$app_dir" ]; then
log_error "App '$name' already exists"
return 1
fi
log_info "Creating app folder: $name"
ensure_dir "$app_dir"
ensure_dir "$app_dir/.streamlit"
# Create template app.py
cat > "$app_dir/app.py" << 'APPTEMPLATE'
import streamlit as st
st.set_page_config(
page_title="My Streamlit App",
page_icon="🚀",
layout="wide"
)
st.title("🚀 My Streamlit App")
st.write("Edit this file to build your app!")
# Example widgets
name = st.text_input("Enter your name:")
if name:
st.success(f"Hello, {name}!")
APPTEMPLATE
# Create empty requirements.txt
cat > "$app_dir/requirements.txt" << 'REQTEMPLATE'
# Add your Python dependencies here, one per line
# Example:
# pandas>=2.0.0
# numpy
# plotly
REQTEMPLATE
# Create .streamlit/config.toml
cat > "$app_dir/.streamlit/config.toml" << 'CONFIGTEMPLATE'
[theme]
base = "dark"
primaryColor = "#0ff"
[server]
headless = true
CONFIGTEMPLATE
# Register in UCI
uci set "${CONFIG}.${name}=app"
uci set "${CONFIG}.${name}.name=$name"
uci set "${CONFIG}.${name}.enabled=1"
uci commit "$CONFIG"
log_info "App '$name' created at $app_dir"
# Auto-push to Gitea if enabled
if [ "$gitea_enabled" = "1" ] && [ -n "$gitea_token" ]; then
log_info "Auto-pushing to Gitea..."
cmd_gitea_push "$name"
fi
log_info "Next steps:"
log_info " 1. Edit $app_dir/app.py"
log_info " 2. Add dependencies to $app_dir/requirements.txt"
log_info " 3. Start with: streamlitctl instance add $name <port>"
}
cmd_app_delete() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "App name required"; return 1; }
if [ "$name" = "hello" ]; then
log_error "Cannot delete the default hello app"
return 1
fi
local app_dir="$APPS_PATH/$name"
# Stop any running instance
cmd_instance_stop "$name" 2>/dev/null
# Remove directory
if [ -d "$app_dir" ]; then
rm -rf "$app_dir"
log_info "App folder removed: $app_dir"
fi
# Remove legacy file
if [ -f "$APPS_PATH/${name}.py" ]; then
rm -f "$APPS_PATH/${name}.py"
fi
# Remove from UCI
uci -q delete "${CONFIG}.${name}" 2>/dev/null
uci commit "$CONFIG"
log_info "App '$name' deleted"
# Regenerate GK2 Hub landing page if generator exists
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}
cmd_app_deploy() {
require_root
load_config
local name="$1"
local source="$2"
[ -z "$name" ] || [ -z "$source" ] && {
log_error "Usage: streamlitctl app deploy <name> <source_path>"
return 1
}
local app_dir="$APPS_PATH/$name"
ensure_dir "$app_dir"
if [ -d "$source" ]; then
# Copy directory contents
log_info "Deploying from directory: $source"
cp -r "$source"/* "$app_dir/"
elif [ -f "$source" ]; then
case "$source" in
*.tar.gz|*.tgz)
log_info "Extracting archive: $source"
tar -xzf "$source" -C "$app_dir"
;;
*.zip)
log_info "Extracting zip: $source"
unzip -q "$source" -d "$app_dir"
;;
*.py)
log_info "Deploying single file: $source"
cp "$source" "$app_dir/app.py"
;;
*)
log_error "Unsupported file type"
return 1
;;
esac
else
log_error "Source not found: $source"
return 1
fi
# Register in UCI
uci set "${CONFIG}.${name}=app"
uci set "${CONFIG}.${name}.name=$name"
uci set "${CONFIG}.${name}.enabled=1"
uci commit "$CONFIG"
log_info "App '$name' deployed to $app_dir"
# Auto-push to Gitea if enabled
if [ "$gitea_enabled" = "1" ] && [ -n "$gitea_token" ]; then
log_info "Auto-pushing to Gitea..."
cmd_gitea_push "$name"
fi
# Auto-package for P2P distribution
if [ -x /usr/sbin/secubox-content-pkg ]; then
log_info "Packaging app for P2P distribution..."
/usr/sbin/secubox-content-pkg streamlit "$name" 2>/dev/null && \
log_info "App packaged for mesh distribution"
fi
}
# Instance management
cmd_instance_list() {
load_config
echo "Streamlit Instances:"
echo "--------------------"
local found=0
_print_instance() {
local section="$1"
local name app port enabled
config_get name "$section" name "$section"
config_get app "$section" app ""
config_get port "$section" port ""
config_get enabled "$section" enabled "0"
[ -z "$app" ] || [ -z "$port" ] && return
found=1
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
local running=""
if lxc_running && [ -f "$LXC_ROOTFS/var/run/streamlit/${app}.pid" ]; then
running=" [RUNNING]"
fi
printf " %-15s app:%-15s port:%-5s [%s]%s\n" "$section" "$app" "$port" "$status" "$running"
}
config_load "$CONFIG"
config_foreach _print_instance instance
[ "$found" = "0" ] && echo " (no instances configured)"
}
cmd_instance_add() {
require_root
load_config
local app="$1"
local port="$2"
[ -z "$app" ] || [ -z "$port" ] && {
log_error "Usage: streamlitctl instance add <app_name> <port>"
return 1
}
# Validate port
echo "$port" | grep -qE '^[0-9]+$' || {
log_error "Port must be a number"
return 1
}
# Check app exists
if [ ! -d "$APPS_PATH/$app" ] && [ ! -f "$APPS_PATH/${app}.py" ]; then
log_error "App not found: $app"
log_info "Create with: streamlitctl app create $app"
return 1
fi
# Use app name as instance name
local instance_name="$app"
uci set "${CONFIG}.${instance_name}=instance"
uci set "${CONFIG}.${instance_name}.name=$instance_name"
uci set "${CONFIG}.${instance_name}.app=$app"
uci set "${CONFIG}.${instance_name}.port=$port"
uci set "${CONFIG}.${instance_name}.enabled=1"
uci commit "$CONFIG"
log_info "Instance added: $app on port $port"
log_info "Restart service to apply: /etc/init.d/streamlit restart"
# Regenerate GK2 Hub landing page if generator exists
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}
cmd_instance_remove() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
cmd_instance_stop "$name" 2>/dev/null
uci -q delete "${CONFIG}.${name}" 2>/dev/null || {
log_error "Instance not found: $name"
return 1
}
uci commit "$CONFIG"
# Regenerate instances.conf
generate_instances_conf
log_info "Instance '$name' removed"
# Regenerate GK2 Hub landing page if generator exists
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}
cmd_instance_start() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
# Get instance config
local app=$(uci_get "${name}.app")
local port=$(uci_get "${name}.port")
[ -z "$app" ] || [ -z "$port" ] && {
log_error "Instance '$name' not found or incomplete config"
return 1
}
if ! lxc_running; then
log_error "Container not running. Start with: /etc/init.d/streamlit start"
return 1
fi
log_info "Starting instance '$name' (app: $app, port: $port)..."
lxc_exec sh -c "
WORK_DIR=''
APP_FILE=''
if [ -d /srv/apps/$app ]; then
WORK_DIR='/srv/apps/$app'
cd \"\$WORK_DIR\"
# Find main file in folder
for f in app.py main.py ${app}.py; do
[ -f \"\$f\" ] && { APP_FILE=\"\$f\"; break; }
done
[ -z \"\$APP_FILE\" ] && APP_FILE=\$(ls -1 *.py 2>/dev/null | head -1)
# Install requirements (requirements.txt or any requirements*.txt)
REQ_FILE=''
[ -f requirements.txt ] && REQ_FILE='requirements.txt'
[ -z \"\$REQ_FILE\" ] && REQ_FILE=\$(ls -1 requirements*.txt 2>/dev/null | head -1)
[ -n \"\$REQ_FILE\" ] && pip3 install --break-system-packages -r \"\$REQ_FILE\" 2>/dev/null
elif [ -f /srv/apps/${app}.py ]; then
WORK_DIR='/srv/apps'
APP_FILE='${app}.py'
cd \"\$WORK_DIR\"
fi
[ -z \"\$APP_FILE\" ] && { echo 'No Python file found for $app'; exit 1; }
# Kill existing
[ -f /var/run/streamlit/${app}.pid ] && kill \$(cat /var/run/streamlit/${app}.pid) 2>/dev/null
mkdir -p /var/run/streamlit /var/log/streamlit
nohup streamlit run \"\$APP_FILE\" \
--server.address=0.0.0.0 \
--server.port=$port \
--server.headless=true \
> /var/log/streamlit/${app}.log 2>&1 &
echo \$! > /var/run/streamlit/${app}.pid
echo \"Started \$APP_FILE on port $port (PID: \$!)\"
"
}
cmd_instance_stop() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "Instance name required"; return 1; }
local app=$(uci_get "${name}.app")
[ -z "$app" ] && app="$name"
if ! lxc_running; then
return 0
fi
log_info "Stopping instance '$name'..."
lxc_exec sh -c "
if [ -f /var/run/streamlit/${app}.pid ]; then
kill \$(cat /var/run/streamlit/${app}.pid) 2>/dev/null
rm -f /var/run/streamlit/${app}.pid
echo 'Stopped'
else
echo 'Not running'
fi
"
}
# Gitea integration
cmd_gitea_setup() {
require_root
load_config
if [ -z "$gitea_token" ]; then
log_error "Gitea token not configured"
log_info "Set with: uci set streamlit.gitea.token='your-token' && uci commit streamlit"
return 1
fi
if ! lxc_running; then
log_error "Container not running"
return 1
fi
log_info "Configuring git credentials..."
local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||')
lxc_exec sh -c "
git config --global user.name '$gitea_user'
git config --global user.email '${gitea_user}@localhost'
git config --global credential.helper store
rm -rf ~/.git-credentials
cat > ~/.git-credentials << CRED
https://${gitea_user}:${gitea_token}@${gitea_host}
http://${gitea_user}:${gitea_token}@${gitea_host}
CRED
chmod 600 ~/.git-credentials
"
log_info "Git credentials configured"
}
cmd_gitea_clone() {
require_root
load_config
local name="$1"
local repo="$2"
[ -z "$name" ] || [ -z "$repo" ] && {
log_error "Usage: streamlitctl gitea clone <app_name> <repo>"
log_info " repo can be: username/reponame or full URL"
return 1
}
if [ "$gitea_enabled" != "1" ]; then
log_error "Gitea integration not enabled"
log_info "Enable with: uci set streamlit.gitea.enabled=1 && uci commit streamlit"
return 1
fi
if [ -z "$gitea_token" ]; then
log_error "Gitea token not configured"
return 1
fi
local app_dir="$APPS_PATH/$name"
# Build clone URL
local clone_url
if echo "$repo" | grep -q '^http'; then
clone_url="$repo"
else
local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||')
clone_url="http://${gitea_user}:${gitea_token}@${gitea_host}/${repo}.git"
fi
if [ -d "$app_dir/.git" ]; then
log_info "Repository already cloned, pulling latest..."
cd "$app_dir" && git pull
else
log_info "Cloning $repo to $name..."
ensure_dir "$(dirname "$app_dir")"
rm -rf "$app_dir"
git clone "$clone_url" "$app_dir" || {
log_error "Failed to clone repository"
return 1
}
fi
# Register in UCI
uci set "${CONFIG}.${name}=app"
uci set "${CONFIG}.${name}.name=$name"
uci set "${CONFIG}.${name}.repo=$repo"
uci set "${CONFIG}.${name}.enabled=1"
uci commit "$CONFIG"
log_info "App '$name' cloned from Gitea"
log_info "Add instance with: streamlitctl instance add $name <port>"
}
cmd_gitea_pull() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "App name required"; return 1; }
local app_dir="$APPS_PATH/$name"
if [ ! -d "$app_dir/.git" ]; then
log_error "App '$name' is not a git repository"
return 1
fi
log_info "Pulling latest for '$name'..."
cd "$app_dir" && git pull
log_info "Update complete"
}
# Create Gitea repo via API and push local content
cmd_gitea_push() {
require_root
load_config
local name="$1"
[ -z "$name" ] && { log_error "App name required"; return 1; }
if [ "$gitea_enabled" != "1" ]; then
log_error "Gitea integration not enabled"
log_info "Enable with: uci set streamlit.gitea.enabled=1 && uci commit streamlit"
return 1
fi
if [ -z "$gitea_token" ]; then
log_error "Gitea token not configured"
return 1
fi
local app_dir="$APPS_PATH/$name"
if [ ! -d "$app_dir" ]; then
log_error "App '$name' not found at $app_dir"
return 1
fi
local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||')
local gitea_proto=$(echo "$gitea_url" | grep -q '^https' && echo "https" || echo "http")
local repo_name="streamlit-$name"
log_info "Creating Gitea repository: $repo_name"
# Check if repo exists, create if not
local repo_check=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token $gitea_token" \
"${gitea_url}/api/v1/repos/${gitea_user}/${repo_name}" 2>/dev/null)
if [ "$repo_check" != "200" ]; then
log_info "Repository doesn't exist, creating..."
local create_result=$(curl -s -X POST \
-H "Authorization: token $gitea_token" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${repo_name}\",\"description\":\"Streamlit app: ${name}\",\"private\":true,\"auto_init\":false}" \
"${gitea_url}/api/v1/user/repos" 2>/dev/null)
if ! echo "$create_result" | grep -q "\"name\":"; then
log_error "Failed to create repository"
log_error "Response: $create_result"
return 1
fi
log_info "Repository created: ${gitea_user}/${repo_name}"
else
log_info "Repository exists: ${gitea_user}/${repo_name}"
fi
# Initialize git in app directory if needed
cd "$app_dir"
if [ ! -d ".git" ]; then
log_info "Initializing git repository..."
git init
git config user.name "$gitea_user"
git config user.email "${gitea_user}@localhost"
fi
# Set remote
local remote_url="${gitea_proto}://${gitea_user}:${gitea_token}@${gitea_host}/${gitea_user}/${repo_name}.git"
git remote remove origin 2>/dev/null
git remote add origin "$remote_url"
# Add, commit and push
log_info "Adding files and committing..."
git add -A
git commit -m "Auto-push from SecuBox Streamlit at $(date -Iseconds)" 2>/dev/null || \
log_info "No changes to commit"
log_info "Pushing to Gitea..."
git push -u origin HEAD:main --force 2>&1 || {
# Try master branch as fallback
git push -u origin HEAD:master --force 2>&1 || {
log_error "Failed to push to Gitea"
return 1
}
}
# Save repo reference in UCI
uci set "${CONFIG}.${name}.repo=${gitea_user}/${repo_name}"
uci set "${CONFIG}.${name}.gitea_synced=$(date -Iseconds)"
uci commit "$CONFIG"
log_info "Push complete: ${gitea_url}/${gitea_user}/${repo_name}"
}
# Initialize Gitea for all existing apps
cmd_gitea_init_all() {
require_root
load_config
if [ "$gitea_enabled" != "1" ]; then
log_error "Gitea integration not enabled"
log_info "Enable with: uci set streamlit.gitea.enabled=1 && uci commit streamlit"
return 1
fi
if [ -z "$gitea_token" ]; then
log_error "Gitea token not configured"
return 1
fi
log_info "Initializing Gitea repositories for all apps..."
echo ""
local success=0
local failed=0
# Process all app directories
if [ -d "$APPS_PATH" ]; then
for app_dir in "$APPS_PATH"/*/; do
[ -d "$app_dir" ] || continue
local name=$(basename "$app_dir")
# Skip if already has a repo configured
local existing_repo=$(uci -q get ${CONFIG}.${name}.repo)
if [ -n "$existing_repo" ]; then
log_info "[$name] Already linked to $existing_repo, syncing..."
cmd_gitea_push "$name" && success=$((success + 1)) || failed=$((failed + 1))
else
log_info "[$name] Creating Gitea repository..."
cmd_gitea_push "$name" && success=$((success + 1)) || failed=$((failed + 1))
fi
echo ""
done
fi
echo "========================================"
echo "Gitea initialization complete"
echo " Success: $success"
echo " Failed: $failed"
echo "========================================"
}
# ===========================================
# KISS ULTIME MODE - Emancipate
# ===========================================
_emancipate_dns() {
local name="$1"
local domain="$2"
local default_zone=$(uci -q get dns-provider.main.zone)
local provider=$(uci -q get dns-provider.main.provider)
local vortex_wildcard=$(uci -q get vortex-dns.master.wildcard_domain)
# Check if dnsctl is available
if ! command -v dnsctl >/dev/null 2>&1; then
log_warn "[DNS] dnsctl not found, skipping external DNS"
return 1
fi
# Get public IP
local public_ip=$(curl -s --connect-timeout 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\n')
[ -z "$public_ip" ] && { log_warn "[DNS] Cannot detect public IP, skipping DNS"; return 1; }
# Detect zone from domain suffix (try known zones)
local zone=""
local subdomain=""
for z in "secubox.in" "maegia.tv" "cybermind.fr"; do
if echo "$domain" | grep -q "\.${z}$"; then
zone="$z"
subdomain=$(echo "$domain" | sed "s/\.${z}$//")
break
elif [ "$domain" = "$z" ]; then
zone="$z"
subdomain="@"
break
fi
done
# Fallback to default zone if no match
if [ -z "$zone" ]; then
zone="$default_zone"
subdomain=$(echo "$domain" | sed "s/\.${zone}$//")
fi
[ -z "$zone" ] && { log_warn "[DNS] No zone detected, skipping external DNS"; return 1; }
log_info "[DNS] Registering $subdomain.$zone -> $public_ip via $provider"
# Register on the published domain's zone
dnsctl -z "$zone" add A "$subdomain" "$public_ip" 3600
# Also register on vortex node subdomain (e.g., myapp.gk2.secubox.in)
if [ -n "$vortex_wildcard" ]; then
local vortex_zone=$(echo "$vortex_wildcard" | sed 's/^[^.]*\.//')
local vortex_node=$(echo "$vortex_wildcard" | cut -d. -f1)
local vortex_subdomain="${name}.${vortex_node}"
log_info "[DNS] Registering $vortex_subdomain.$vortex_zone -> $public_ip (vortex node)"
dnsctl -z "$vortex_zone" add A "$vortex_subdomain" "$public_ip" 3600
fi
log_info "[DNS] Verify with: dnsctl verify $domain"
}
_emancipate_vortex() {
local name="$1"
local domain="$2"
# Check if vortexctl is available
if ! command -v vortexctl >/dev/null 2>&1; then
log_info "[VORTEX] vortexctl not found, skipping mesh publication"
return 0
fi
# Check if vortex-dns is enabled
local vortex_enabled=$(uci -q get vortex-dns.main.enabled)
if [ "$vortex_enabled" = "1" ]; then
log_info "[VORTEX] Publishing $name as $domain to mesh"
vortexctl mesh publish "$name" "$domain" 2>/dev/null
else
log_info "[VORTEX] Vortex DNS disabled, skipping mesh publication"
fi
}
_emancipate_haproxy() {
local name="$1"
local domain="$2"
local port="$3"
log_info "[HAPROXY] Creating vhost for $domain -> port $port"
# Create backend
local backend_name="streamlit_${name}"
uci set haproxy.${backend_name}=backend
uci set haproxy.${backend_name}.name="$backend_name"
uci set haproxy.${backend_name}.mode="http"
uci set haproxy.${backend_name}.balance="roundrobin"
uci set haproxy.${backend_name}.enabled="1"
# Create server
local server_name="${backend_name}_srv"
uci set haproxy.${server_name}=server
uci set haproxy.${server_name}.backend="$backend_name"
uci set haproxy.${server_name}.name="streamlit"
uci set haproxy.${server_name}.address="192.168.255.1"
uci set haproxy.${server_name}.port="$port"
uci set haproxy.${server_name}.weight="100"
uci set haproxy.${server_name}.check="1"
uci set haproxy.${server_name}.enabled="1"
# Create vhost with SSL
local vhost_name=$(echo "$domain" | tr '.-' '_')
uci set haproxy.${vhost_name}=vhost
uci set haproxy.${vhost_name}.domain="$domain"
uci set haproxy.${vhost_name}.backend="$backend_name"
uci set haproxy.${vhost_name}.ssl="1"
uci set haproxy.${vhost_name}.ssl_redirect="1"
uci set haproxy.${vhost_name}.acme="1"
uci set haproxy.${vhost_name}.enabled="1"
uci commit haproxy
# Generate HAProxy config
if command -v haproxyctl >/dev/null 2>&1; then
haproxyctl generate 2>/dev/null
fi
}
_emancipate_ssl() {
local domain="$1"
log_info "[SSL] Requesting certificate for $domain"
# Check if haproxyctl is available
if ! command -v haproxyctl >/dev/null 2>&1; then
log_warn "[SSL] haproxyctl not found, skipping SSL"
return 1
fi
# haproxyctl cert add handles ACME webroot mode
haproxyctl cert add "$domain" 2>&1 | while read line; do
echo " $line"
done
if [ -f "/srv/haproxy/certs/$domain.pem" ]; then
log_info "[SSL] Certificate obtained successfully"
else
log_warn "[SSL] Certificate request may still be pending"
log_warn "[SSL] Check with: haproxyctl cert verify $domain"
fi
}
_emancipate_reload() {
log_info "[RELOAD] Applying HAProxy configuration"
# Generate fresh config
haproxyctl generate 2>/dev/null
# Restart for clean state with new vhosts/certs
log_info "[RELOAD] Restarting HAProxy for clean state..."
/etc/init.d/haproxy restart 2>/dev/null
sleep 1
# Verify HAProxy is running
if pgrep haproxy >/dev/null 2>&1; then
log_info "[RELOAD] HAProxy restarted successfully"
else
log_warn "[RELOAD] HAProxy may not have started properly"
fi
# Regenerate GK2 Hub landing page if generator exists
[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}
_emancipate_acl() {
local name="$1"
local domain="$2"
local port="$3"
log_info "[ACL] Creating routing rules for $name"
# Create ACL rule for Streamlit WebSocket paths
local acl_name="streamlit_${name}_acl"
local vhost_name=$(echo "$domain" | tr '.-' '_')
local backend_name="streamlit_${name}"
# ACL for Streamlit-specific paths (WebSocket + static)
uci set haproxy.${acl_name}=acl
uci set haproxy.${acl_name}.name="path_streamlit_${name}"
uci set haproxy.${acl_name}.criterion="path_beg"
uci set haproxy.${acl_name}.pattern="/_stcore /static /component /media /health"
uci set haproxy.${acl_name}.backend="$backend_name"
uci set haproxy.${acl_name}.vhost="$vhost_name"
uci set haproxy.${acl_name}.enabled="1"
uci commit haproxy
log_info "[ACL] Rule $acl_name created for vhost $vhost_name"
}
_emancipate_mesh() {
local name="$1"
local domain="$2"
local port="$3"
log_info "[MESH] Publishing $name to P2P mesh"
# Register in secubox-p2p shared services
if command -v ubus >/dev/null 2>&1; then
ubus call luci.secubox-p2p share_service "{\"name\":\"streamlit_${name}\",\"port\":$port,\"domain\":\"$domain\",\"type\":\"streamlit\"}" 2>/dev/null && \
log_info "[MESH] Service published to local mesh registry"
fi
# Sync to peers if P2P is enabled
local p2p_enabled=$(uci -q get secubox-p2p.main.enabled)
if [ "$p2p_enabled" = "1" ]; then
# Get peer list and sync
local peers=$(uci -q get secubox-p2p.main.peers 2>/dev/null)
for peer in $peers; do
local peer_addr=$(uci -q get secubox-p2p.${peer}.address 2>/dev/null)
if [ -n "$peer_addr" ]; then
log_info "[MESH] Syncing to peer: $peer_addr"
# Use dbclient to sync emancipation state
dbclient -y -i /root/.ssh/id_dropbear "root@$peer_addr" \
"uci set streamlit.${name}_remote=remote_instance; \
uci set streamlit.${name}_remote.origin_host='$(uci -q get system.@system[0].hostname)'; \
uci set streamlit.${name}_remote.domain='$domain'; \
uci set streamlit.${name}_remote.port='$port'; \
uci commit streamlit" 2>/dev/null && \
log_info "[MESH] Peer $peer_addr updated" || \
log_warn "[MESH] Failed to sync to $peer_addr"
fi
done
fi
}
cmd_emancipate_all() {
require_root
load_config
echo ""
echo "=============================================="
echo " BULK EMANCIPATION: All Streamlit Instances"
echo "=============================================="
echo ""
# Get all enabled instances
local instances=$(uci show ${CONFIG} 2>/dev/null | grep "=instance$" | cut -d'.' -f2 | cut -d'=' -f1)
local count=0
local success=0
local failed=0
for inst in $instances; do
local enabled=$(uci -q get ${CONFIG}.${inst}.enabled)
local port=$(uci -q get ${CONFIG}.${inst}.port)
local app=$(uci -q get ${CONFIG}.${inst}.app)
if [ "$enabled" = "1" ] && [ -n "$port" ]; then
count=$((count + 1))
echo "[$count] Emancipating: $inst (app: $app, port: $port)"
# Check if already emancipated
local already=$(uci -q get ${CONFIG}.${inst}.emancipated)
if [ "$already" = "1" ]; then
local existing_domain=$(uci -q get ${CONFIG}.${inst}.domain)
echo " Already emancipated: $existing_domain"
success=$((success + 1))
continue
fi
# Run emancipation
if cmd_emancipate "$inst" 2>/dev/null; then
success=$((success + 1))
echo " ✓ Success"
else
failed=$((failed + 1))
echo " ✗ Failed"
fi
fi
done
echo ""
echo "=============================================="
echo " BULK EMANCIPATION COMPLETE"
echo "=============================================="
echo ""
echo " Total: $count instances"
echo " Success: $success"
echo " Failed: $failed"
echo ""
# Final reload
_emancipate_reload
}
cmd_emancipate() {
require_root
load_config
local name="$1"
local domain="$2"
[ -z "$name" ] && { log_error "Instance name required"; usage; return 1; }
# Get instance port and app name from UCI
local port=$(uci -q get ${CONFIG}.${name}.port)
local app=$(uci -q get ${CONFIG}.${name}.app)
# If no port, this might be an app name not instance name
if [ -z "$port" ]; then
# Try to find an instance for this app
local found_inst=$(uci show ${CONFIG} 2>/dev/null | grep "\.app='$name'" | head -1 | cut -d'.' -f2)
if [ -n "$found_inst" ]; then
port=$(uci -q get ${CONFIG}.${found_inst}.port)
app="$name"
name="$found_inst"
fi
fi
# Check if app folder exists (use app name, not instance name)
local app_to_check="${app:-$name}"
if [ ! -d "$APPS_PATH/$app_to_check" ] && [ ! -f "$APPS_PATH/${app_to_check}.py" ]; then
log_error "App '$app_to_check' not found in $APPS_PATH"
log_error "Create first: streamlitctl app create $app_to_check"
return 1
fi
if [ -z "$port" ]; then
log_error "No instance configured for app '$name'"
log_error "Add instance first: streamlitctl instance add $name <port>"
return 1
fi
# Domain is optional - can be auto-generated from vortex wildcard
if [ -z "$domain" ]; then
local vortex_wildcard=$(uci -q get vortex-dns.master.wildcard_domain)
if [ -n "$vortex_wildcard" ]; then
local vortex_zone=$(echo "$vortex_wildcard" | sed 's/^[^.]*\.//')
local vortex_node=$(echo "$vortex_wildcard" | cut -d. -f1)
domain="${name}.${vortex_node}.${vortex_zone}"
log_info "Auto-generated domain: $domain"
else
log_error "Domain required: streamlitctl emancipate $name <domain>"
return 1
fi
fi
echo ""
echo "=============================================="
echo " KISS ULTIME MODE: Emancipating $name"
echo "=============================================="
echo ""
echo " App: $name"
echo " Domain: $domain"
echo " Port: $port"
echo ""
# Step 1: DNS Registration (external provider)
_emancipate_dns "$name" "$domain"
# Step 2: Vortex DNS (mesh registration)
_emancipate_vortex "$name" "$domain"
# Step 3: HAProxy vhost + backend
_emancipate_haproxy "$name" "$domain" "$port"
# Step 4: HAProxy ACL rules for routing
_emancipate_acl "$name" "$domain" "$port"
# Step 5: SSL Certificate
_emancipate_ssl "$domain"
# Step 6: Mesh P2P distribution to peers
_emancipate_mesh "$name" "$domain" "$port"
# Step 7: Reload HAProxy
_emancipate_reload
# Mark app as emancipated
uci set ${CONFIG}.${name}.emancipated="1"
uci set ${CONFIG}.${name}.emancipated_at="$(date -Iseconds)"
uci set ${CONFIG}.${name}.domain="$domain"
uci commit ${CONFIG}
echo ""
echo "=============================================="
echo " EMANCIPATION COMPLETE"
echo "=============================================="
echo ""
echo " Site: https://$domain"
echo " Status: Published and SSL-protected"
echo " Mesh: $(uci -q get vortex-dns.main.enabled | grep -q 1 && echo 'Published' || echo 'Disabled')"
echo ""
echo " Verify:"
echo " curl -v https://$domain"
echo " dnsctl verify $domain"
echo " haproxyctl cert verify $domain"
echo ""
}
# Main commands
cmd_install() {
require_root
load_config
log_info "Installing Streamlit Platform..."
lxc_check_prereqs || exit 1
if ! lxc_exists; then
lxc_create_rootfs || exit 1
fi
lxc_create_config || exit 1
create_startup_script
# Setup default app
ensure_dir "$APPS_PATH/hello"
uci_set main.enabled '1'
/etc/init.d/streamlit enable 2>/dev/null || true
log_info "Installation complete!"
log_info ""
log_info "Next steps:"
log_info " 1. Create app: streamlitctl app create myapp"
log_info " 2. Add instance: streamlitctl instance add myapp 8501"
log_info " 3. Start: /etc/init.d/streamlit start"
}
cmd_uninstall() {
require_root
log_info "Uninstalling Streamlit Platform..."
/etc/init.d/streamlit stop 2>/dev/null || true
/etc/init.d/streamlit disable 2>/dev/null || true
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "Container removed"
fi
uci_set main.enabled '0'
log_info "Streamlit Platform uninstalled"
log_info "Apps preserved in: $(uci_get main.data_path)/apps"
}
cmd_update() {
require_root
load_config
if ! lxc_exists; then
log_error "Container not installed"
return 1
fi
log_info "Updating Streamlit..."
rm -f "$LXC_ROOTFS/opt/.installed"
if [ "$(uci_get main.enabled)" = "1" ]; then
/etc/init.d/streamlit restart
fi
log_info "Update will apply on next start"
}
cmd_status() {
load_config
local enabled="$(uci_get main.enabled)"
local running="false"
lxc_running && running="true"
cat << EOF
Streamlit Platform Status
==========================
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
Running: $([ "$running" = "true" ] && echo "yes" || echo "no")
Data Path: $data_path
Memory: $memory_limit
Container: $LXC_NAME
EOF
echo "Apps:"
cmd_app_list | tail -n +3
echo ""
echo "Instances:"
cmd_instance_list | tail -n +3
}
cmd_logs() {
load_config
local app="${1:-}"
if [ -n "$app" ]; then
local log_file="$data_path/logs/${app}.log"
if [ -f "$log_file" ]; then
tail -100 "$log_file"
else
# Try inside container
lxc_exec cat /var/log/streamlit/${app}.log 2>/dev/null || echo "No logs for '$app'"
fi
else
# Show all logs
for log_file in "$data_path/logs"/*.log; do
[ -f "$log_file" ] || continue
echo "=== $(basename "$log_file") ==="
tail -50 "$log_file"
echo ""
done
fi
}
cmd_shell() {
require_root
if ! lxc_running; then
log_error "Container not running"
exit 1
fi
lxc-attach -n "$LXC_NAME" -- /bin/sh
}
cmd_service_run() {
require_root
load_config
lxc_check_prereqs || exit 1
lxc_run
}
cmd_service_stop() {
require_root
lxc_stop
}
# Main
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
app)
shift
case "${1:-}" in
list) shift; cmd_app_list "$@" ;;
create) shift; cmd_app_create "$@" ;;
delete) shift; cmd_app_delete "$@" ;;
deploy) shift; cmd_app_deploy "$@" ;;
*) echo "Usage: streamlitctl app {list|create|delete|deploy}"; exit 1 ;;
esac
;;
instance)
shift
case "${1:-}" in
list) shift; cmd_instance_list "$@" ;;
add) shift; cmd_instance_add "$@" ;;
remove) shift; cmd_instance_remove "$@" ;;
start) shift; cmd_instance_start "$@" ;;
stop) shift; cmd_instance_stop "$@" ;;
*) echo "Usage: streamlitctl instance {list|add|remove|start|stop}"; exit 1 ;;
esac
;;
gitea)
shift
case "${1:-}" in
setup) shift; cmd_gitea_setup "$@" ;;
clone) shift; cmd_gitea_clone "$@" ;;
pull) shift; cmd_gitea_pull "$@" ;;
push) shift; cmd_gitea_push "$@" ;;
init-all) shift; cmd_gitea_init_all "$@" ;;
*) echo "Usage: streamlitctl gitea {setup|clone|pull|push|init-all}"; exit 1 ;;
esac
;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
emancipate) shift; cmd_emancipate "$@" ;;
emancipate-all) cmd_emancipate_all ;;
*) usage ;;
esac