v0.2.2.2 (#830)
This commit is contained in:
MartinNYHC
2026-03-27 09:27:47 +01:00
committed by GitHub
parent 8270030687
commit 67f91d94de
112 changed files with 18595 additions and 9993 deletions
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Generate a polished SpoolBuddy boot splash image (1024x600).
Uses the SpoolBuddy logo with baked-in glow, radial gradient background,
subtle light rays, and vignette effects for a premium kiosk boot screen.
Usage:
python3 generate_splash.py [output_path]
Requires: Pillow (pip install Pillow)
"""
import math
import os
import sys
from PIL import Image, ImageDraw, ImageFilter
# --- Configuration ---
WIDTH, HEIGHT = 1024, 600
BG_CENTER = (55, 55, 55) # Notably lighter center
BG_EDGE = (2, 2, 2) # Nearly black edges
ACCENT = (0, 200, 75) # Brighter SpoolBuddy green
ACCENT_GLOW = (0, 255, 100) # Vivid glow core
LOGO_SCALE = 0.50 # Scale logo to 50% of canvas width
GLOW_RADIUS = 160 # Very wide glow spread
VIGNETTE_STRENGTH = 0.85 # Heavy edge darkening
RAY_COUNT = 32 # More radial light rays
RAY_OPACITY = 50 # Clearly visible rays (0-255)
def radial_gradient(size, center_color, edge_color):
"""Create a radial gradient from center to edges."""
w, h = size
img = Image.new("RGB", size)
pixels = img.load()
cx, cy = w // 2, h // 2
max_dist = math.sqrt(cx**2 + cy**2)
for y in range(h):
for x in range(w):
dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
t = min(dist / max_dist, 1.0)
# Ease-out curve for smoother falloff
t = t * t
r = int(center_color[0] + (edge_color[0] - center_color[0]) * t)
g = int(center_color[1] + (edge_color[1] - center_color[1]) * t)
b = int(center_color[2] + (edge_color[2] - center_color[2]) * t)
pixels[x, y] = (r, g, b)
return img
def create_light_rays(size, num_rays, opacity):
"""Create subtle radial light rays emanating from center."""
w, h = size
rays = Image.new("RGBA", size, (0, 0, 0, 0))
draw = ImageDraw.Draw(rays)
cx, cy = w // 2, h // 2
max_radius = int(math.sqrt(cx**2 + cy**2)) + 50
for i in range(num_rays):
angle = (2 * math.pi * i) / num_rays
# Vary ray width slightly for organic feel
half_width = math.radians(1.5 + (i % 3) * 0.5)
a1 = angle - half_width
a2 = angle + half_width
points = [
(cx, cy),
(cx + int(max_radius * math.cos(a1)), cy + int(max_radius * math.sin(a1))),
(cx + int(max_radius * math.cos(a2)), cy + int(max_radius * math.sin(a2))),
]
# Green-tinted rays
draw.polygon(points, fill=(ACCENT[0], ACCENT[1], ACCENT[2], opacity))
# Heavy blur to make rays soft and diffuse
rays = rays.filter(ImageFilter.GaussianBlur(radius=30))
return rays
def create_vignette(size, strength):
"""Create a vignette (edge darkening) mask."""
w, h = size
vignette = Image.new("L", size, 255)
pixels = vignette.load()
cx, cy = w / 2, h / 2
max_dist = math.sqrt(cx**2 + cy**2)
for y in range(h):
for x in range(w):
dist = math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
t = dist / max_dist
# Ramp darkening from ~40% radius outward
fade = max(0, (t - 0.4) / 0.6)
fade = fade * fade # Quadratic ease
val = int(255 * (1 - fade * strength))
pixels[x, y] = max(0, val)
return vignette
def create_glow(logo_img, color, radius, intensity=1.5):
"""Create a colored glow effect from a logo's alpha channel."""
# Extract alpha as the glow shape
if logo_img.mode != "RGBA":
return Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
alpha = logo_img.split()[3]
# Create colored version of the alpha shape
glow = Image.new("RGBA", logo_img.size, (0, 0, 0, 0))
glow_pixels = glow.load()
alpha_pixels = alpha.load()
for y in range(logo_img.height):
for x in range(logo_img.width):
a = alpha_pixels[x, y]
if a > 0:
boosted = min(255, int(a * intensity))
glow_pixels[x, y] = (color[0], color[1], color[2], boosted)
# Blur to create the glow spread
glow = glow.filter(ImageFilter.GaussianBlur(radius=radius))
return glow
def generate_splash(output_path):
"""Generate the final splash image."""
print(f"Generating {WIDTH}x{HEIGHT} splash image...")
# 1. Radial gradient background
print(" Creating radial gradient background...")
canvas = radial_gradient((WIDTH, HEIGHT), BG_CENTER, BG_EDGE)
# 2. Light rays
print(" Adding light rays...")
rays = create_light_rays((WIDTH, HEIGHT), RAY_COUNT, RAY_OPACITY)
canvas.paste(
Image.alpha_composite(
Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0)),
rays,
),
(0, 0),
rays,
)
# 3. Load and scale logo
print(" Loading SpoolBuddy logo...")
script_dir = os.path.dirname(os.path.abspath(__file__))
logo_paths = [
os.path.join(script_dir, "..", "..", "frontend", "public", "spoolbuddy_logo_dark.png"),
os.path.join(script_dir, "..", "..", "frontend", "public", "img", "spoolbuddy_logo_dark.png"),
]
logo = None
for p in logo_paths:
resolved = os.path.normpath(p)
if os.path.exists(resolved):
logo = Image.open(resolved).convert("RGBA")
print(f" Loaded logo from {resolved}")
break
if logo is None:
print(" ERROR: Could not find spoolbuddy_logo_dark.png")
sys.exit(1)
# Scale logo to target width
target_w = int(WIDTH * LOGO_SCALE)
scale = target_w / logo.width
target_h = int(logo.height * scale)
logo = logo.resize((target_w, target_h), Image.LANCZOS)
# Center position (shift up slightly for visual balance)
logo_x = (WIDTH - target_w) // 2
logo_y = (HEIGHT - target_h) // 2 - 10
# 4. Glow behind logo (two layers: wide diffuse + tight bright)
print(" Rendering glow effects...")
# Wide diffuse glow — very prominent
glow_wide = create_glow(logo, ACCENT, radius=GLOW_RADIUS, intensity=3.0)
glow_canvas = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
glow_canvas.paste(glow_wide, (logo_x, logo_y), glow_wide)
canvas = Image.alpha_composite(canvas.convert("RGBA"), glow_canvas)
# Medium glow layer for extra punch
glow_mid = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 2, intensity=2.5)
glow_canvas_mid = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
glow_canvas_mid.paste(glow_mid, (logo_x, logo_y), glow_mid)
canvas = Image.alpha_composite(canvas, glow_canvas_mid)
# Tighter bright glow core
glow_tight = create_glow(logo, ACCENT_GLOW, radius=GLOW_RADIUS // 4, intensity=2.0)
glow_canvas2 = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0))
glow_canvas2.paste(glow_tight, (logo_x, logo_y), glow_tight)
canvas = Image.alpha_composite(canvas, glow_canvas2)
# 5. Composite logo on top
print(" Compositing logo...")
canvas.paste(logo, (logo_x, logo_y), logo)
# 6. Apply vignette
print(" Applying vignette...")
vignette = create_vignette((WIDTH, HEIGHT), VIGNETTE_STRENGTH)
canvas_rgb = canvas.convert("RGB")
# Multiply canvas by vignette mask
r, g, b = canvas_rgb.split()
r = Image.composite(r, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
g = Image.composite(g, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
b = Image.composite(b, Image.new("L", (WIDTH, HEIGHT), 0), vignette)
canvas = Image.merge("RGB", (r, g, b))
# 7. Save
canvas.save(output_path, "PNG", optimize=True)
file_size = os.path.getsize(output_path) / 1024
print(f" Saved to {output_path} ({file_size:.0f} KB)")
if __name__ == "__main__":
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "splash.png")
generate_splash(out)
+368 -24
View File
@@ -12,10 +12,13 @@
#
# Options:
# --mode MODE Installation mode: "spoolbuddy" (companion only) or "full" (both)
# --repo URL Git repository URL to install from (default: upstream repo)
# --ref REF Git ref to install (branch/tag/commit, default: main)
# --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)
# --api-key KEY Bambuddy API key (required for spoolbuddy mode)
# --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)
# --port PORT Bambuddy port (full mode only, default: 8000)
# --ssh-pubkey KEY Bambuddy SSH public key for remote updates
# --yes, -y Non-interactive mode, accept defaults
# --help, -h Show this help message
#
@@ -49,6 +52,10 @@ SPOOLBUDDY_PIP_PACKAGES="spidev gpiod smbus2 httpx"
INSTALL_MODE="" # "spoolbuddy" or "full"
INSTALL_PATH=""
INSTALL_REPO=""
INSTALL_REF=""
DETECTED_INSTALLER_REPO=""
DETECTED_INSTALLER_REF=""
BAMBUDDY_URL=""
API_KEY=""
BAMBUDDY_PORT="8000"
@@ -56,6 +63,7 @@ NON_INTERACTIVE="false"
REBOOT_NEEDED="false"
KIOSK_USER="" # auto-detected from $SUDO_USER
KIOSK_URL="" # derived from $BAMBUDDY_URL/spoolbuddy?token=$API_KEY
SSH_PUBKEY="" # Bambuddy's SSH public key for remote updates
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
@@ -186,10 +194,13 @@ show_help() {
echo ""
echo "Options:"
echo " --mode MODE \"spoolbuddy\" (companion only) or \"full\" (Bambuddy + SpoolBuddy)"
echo " --repo URL Git repository URL to install from"
echo " --ref REF Git ref to install (branch/tag/commit)"
echo " --bambuddy-url URL Bambuddy server URL (required for spoolbuddy mode)"
echo " --api-key KEY Bambuddy API key (required for spoolbuddy mode)"
echo " --path PATH Installation directory (default: /opt/spoolbuddy or /opt/bambuddy)"
echo " --port PORT Bambuddy port (full mode only, default: 8000)"
echo " --ssh-pubkey KEY Bambuddy SSH public key for remote updates"
echo " --yes, -y Non-interactive mode, accept defaults"
echo " --help, -h Show this help message"
echo ""
@@ -205,6 +216,70 @@ show_help() {
exit 0
}
normalize_github_repo_url() {
local url="$1"
if [[ -z "$url" ]]; then
echo ""
return
fi
# Convert git@github.com:owner/repo(.git) to https://github.com/owner/repo.git
if [[ "$url" =~ ^git@github.com:(.+)$ ]]; then
url="https://github.com/${BASH_REMATCH[1]}"
fi
# Keep remote URL style consistent.
url="${url%.git}"
echo "${url}.git"
}
detect_installer_source_context() {
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if git -C "$script_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
DETECTED_INSTALLER_REF="$(git -C "$script_dir" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
local origin_url
origin_url="$(git -C "$script_dir" remote get-url origin 2>/dev/null || true)"
DETECTED_INSTALLER_REPO="$(normalize_github_repo_url "$origin_url")"
fi
# Optional environment overrides for raw-download installs.
if [[ -n "${SPOOLBUDDY_INSTALL_REPO:-}" ]]; then
DETECTED_INSTALLER_REPO="$(normalize_github_repo_url "$SPOOLBUDDY_INSTALL_REPO")"
fi
if [[ -n "${SPOOLBUDDY_INSTALL_REF:-}" ]]; then
DETECTED_INSTALLER_REF="$SPOOLBUDDY_INSTALL_REF"
fi
if [[ -z "$INSTALL_REPO" ]]; then
if [[ -n "$DETECTED_INSTALLER_REPO" ]]; then
INSTALL_REPO="$DETECTED_INSTALLER_REPO"
else
INSTALL_REPO="$GITHUB_REPO"
fi
fi
if [[ -z "$INSTALL_REF" ]]; then
if [[ -n "$DETECTED_INSTALLER_REF" && "$DETECTED_INSTALLER_REF" != "HEAD" ]]; then
INSTALL_REF="$DETECTED_INSTALLER_REF"
else
INSTALL_REF="main"
fi
fi
}
resolve_install_ref() {
local ref="$1"
# If ref exists on origin as a branch, track/reset it. Otherwise treat it as tag/commit.
if git ls-remote --exit-code --heads origin "$ref" >/dev/null 2>&1; then
git checkout -B "$ref" "origin/$ref" > /dev/null 2>&1
git reset --hard "origin/$ref" > /dev/null 2>&1
else
git checkout "$ref" > /dev/null 2>&1
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Pre-flight Checks
# ─────────────────────────────────────────────────────────────────────────────
@@ -309,21 +384,32 @@ configure_boot_config() {
if [[ ! -f "$boot_config" ]]; then
warn "Boot config not found at /boot/firmware/config.txt or /boot/config.txt"
warn "You may need to manually add: dtparam=i2c_vc=on and dtoverlay=spi0-0cs"
warn "You may need to manually add: dtparam=i2c_arm=on and dtoverlay=spi0-0cs"
return
fi
info "Configuring $boot_config..."
# Enable I2C bus 0 (GPIO0/GPIO1) for NAU7802 scale
if ! grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
echo "" >> "$boot_config"
echo "# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" >> "$boot_config"
echo "dtparam=i2c_vc=on" >> "$boot_config"
# Migrate legacy SpoolBuddy setting (bus 0 / i2c_vc) to bus 1 / i2c_arm.
if grep -q "^dtparam=i2c_vc=on" "$boot_config"; then
sed -i "s/^dtparam=i2c_vc=on$/# dtparam=i2c_vc=on (disabled by SpoolBuddy installer; use i2c_arm bus 1)/" "$boot_config"
REBOOT_NEEDED="true"
success "Added dtparam=i2c_vc=on"
success "Disabled legacy dtparam=i2c_vc=on"
fi
if grep -q "^# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0/GPIO1)" "$boot_config"; then
sed -i "s/^# SpoolBuddy: I2C bus 0 for NAU7802 scale (GPIO0\/GPIO1)$/# SpoolBuddy: I2C bus 1 for NAU7802 scale (GPIO2\/GPIO3)/" "$boot_config"
fi
# Ensure I2C bus 1 (GPIO2/GPIO3) is enabled for NAU7802 scale
if ! grep -q "^dtparam=i2c_arm=on" "$boot_config"; then
echo "" >> "$boot_config"
echo "# SpoolBuddy: I2C bus 1 for NAU7802 scale (GPIO2/GPIO3)" >> "$boot_config"
echo "dtparam=i2c_arm=on" >> "$boot_config"
REBOOT_NEEDED="true"
success "Added dtparam=i2c_arm=on"
else
success "dtparam=i2c_vc=on already set"
success "dtparam=i2c_arm=on already set"
fi
# Disable SPI auto chip-select (manual CS on GPIO23 for PN5180)
@@ -347,6 +433,63 @@ install_system_packages() {
run_with_progress "Installing system packages" apt-get install -y $SYSTEM_PACKAGES
}
install_wifi_safeguard() {
# Protect WiFi credentials from being wiped by apt upgrades.
# Raspberry Pi OS Bookworm migrated from wpa_supplicant/dhcpcd to
# NetworkManager, but certain package upgrades (raspberrypi-sys-mods,
# raspi-config, NetworkManager itself) can delete saved connections
# from /etc/NetworkManager/system-connections/. This hook backs them
# up before dpkg runs and restores them if they vanish.
local hook_file="/etc/apt/apt.conf.d/80-preserve-wifi"
if [[ -f "$hook_file" ]]; then
success "WiFi safeguard already installed"
return
fi
# Only install if NetworkManager is the active network manager
if ! systemctl is-active --quiet NetworkManager 2>/dev/null; then
return
fi
# Write a helper script (avoids quote escaping issues in APT config)
local helper="/usr/local/sbin/preserve-wifi"
cat > "$helper" << 'HELPEREOF'
#!/bin/sh
# Called by APT hooks to preserve NetworkManager WiFi connections.
NM_DIR="/etc/NetworkManager/system-connections"
BAK_DIR="/etc/NetworkManager/system-connections.bak"
case "$1" in
backup)
if [ -d "$NM_DIR" ] && [ -n "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then
cp -a "$NM_DIR/" "$BAK_DIR/"
fi
;;
restore)
if [ -d "$BAK_DIR" ] && [ -z "$(ls -A "$NM_DIR" 2>/dev/null)" ]; then
cp -a "$BAK_DIR"/* "$NM_DIR"/
nmcli general reload 2>/dev/null
fi
rm -rf "$BAK_DIR" 2>/dev/null
;;
esac
HELPEREOF
chmod +x "$helper"
cat > "$hook_file" << 'APTEOF'
// Preserve NetworkManager WiFi connections across apt upgrades.
// Installed by SpoolBuddy.
DPkg::Pre-Invoke {"/usr/local/sbin/preserve-wifi backup";};
DPkg::Post-Invoke {"/usr/local/sbin/preserve-wifi restore";};
APTEOF
success "WiFi safeguard installed (${hook_file})"
}
upgrade_system_packages() {
run_with_progress "Upgrading system packages" apt-get upgrade -y
}
# ─────────────────────────────────────────────────────────────────────────────
# SpoolBuddy Installation
# ─────────────────────────────────────────────────────────────────────────────
@@ -354,9 +497,11 @@ install_system_packages() {
create_spoolbuddy_user() {
if id "$SPOOLBUDDY_SERVICE_USER" &>/dev/null; then
info "User '$SPOOLBUDDY_SERVICE_USER' already exists"
# Ensure existing installs get a real shell for SSH access
usermod --shell /bin/bash "$SPOOLBUDDY_SERVICE_USER" 2>/dev/null || true
else
info "Creating service user '$SPOOLBUDDY_SERVICE_USER'..."
useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
useradd --system --shell /bin/bash --home-dir "$INSTALL_PATH" "$SPOOLBUDDY_SERVICE_USER"
success "Service user created"
fi
@@ -367,6 +512,15 @@ create_spoolbuddy_user() {
fi
done
success "User added to gpio, spi, i2c, video groups"
# Allow passwordless restart of daemon + kiosk (needed for SSH-based updates from Bambuddy)
cat > /etc/sudoers.d/spoolbuddy << 'SUDOERS'
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/systemctl restart spoolbuddy.service
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/systemctl restart getty@tty1.service
spoolbuddy ALL=(root) NOPASSWD: /usr/bin/find /home -maxdepth 5 *
SUDOERS
chmod 440 /etc/sudoers.d/spoolbuddy
success "Sudoers entries created for service and kiosk restart"
}
download_spoolbuddy() {
@@ -374,15 +528,14 @@ download_spoolbuddy() {
info "Existing installation found, updating..."
git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
cd "$INSTALL_PATH"
git remote set-url origin "$INSTALL_REPO" 2>/dev/null || true
run_with_progress "Fetching updates" git fetch origin
git reset --hard origin/main > /dev/null 2>&1
cd "$INSTALL_PATH"
git checkout 0.2.2b1
resolve_install_ref "$INSTALL_REF"
else
mkdir -p "$INSTALL_PATH"
run_with_progress "Cloning repository" git clone "$GITHUB_REPO" "$INSTALL_PATH"
run_with_progress "Cloning repository" git clone "$INSTALL_REPO" "$INSTALL_PATH"
cd "$INSTALL_PATH"
git checkout 0.2.2b1
resolve_install_ref "$INSTALL_REF"
fi
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$INSTALL_PATH"
@@ -412,13 +565,73 @@ SPOOLBUDDY_BACKEND_URL=$BAMBUDDY_URL
# API key (create one in Bambuddy Settings -> API Keys)
SPOOLBUDDY_API_KEY=$API_KEY
# NAU7802 scale bus (RPi GPIO2/GPIO3)
SPOOLBUDDY_I2C_BUS=1
EOF
chown "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$env_file"
chmod 600 "$env_file"
# Keep secrets owner-writable while allowing kiosk user (in spoolbuddy group)
# to read backend URL/API key for dynamic launcher URL resolution.
chgrp "$SPOOLBUDDY_SERVICE_USER" "$env_file"
chmod 640 "$env_file"
success "Configuration saved to $env_file"
}
ensure_kiosk_env_access() {
local env_file="$INSTALL_PATH/spoolbuddy/.env"
if [[ ! -f "$env_file" ]]; then
warn "SpoolBuddy env file not found at $env_file"
return
fi
# Ensure kiosk user is known even when this function is called outside setup_kiosk.
if [[ -z "$KIOSK_USER" ]]; then
KIOSK_USER="${SUDO_USER:-$(logname 2>/dev/null || echo pi)}"
fi
if id "$KIOSK_USER" &>/dev/null; then
usermod -aG "$SPOOLBUDDY_SERVICE_USER" "$KIOSK_USER" 2>/dev/null || true
fi
chgrp "$SPOOLBUDDY_SERVICE_USER" "$env_file"
chmod 640 "$env_file"
if ! su -s /bin/sh -c "test -r '$env_file'" "$KIOSK_USER"; then
error "Kiosk user '$KIOSK_USER' cannot read $env_file (required for dynamic kiosk URL). Check groups/permissions."
fi
success "Verified kiosk user '$KIOSK_USER' can read SpoolBuddy env"
}
setup_ssh_key() {
info "Setting up SSH access for Bambuddy remote updates..."
local ssh_dir="$INSTALL_PATH/.ssh"
local auth_keys="$ssh_dir/authorized_keys"
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
if [[ -n "$SSH_PUBKEY" ]]; then
# Manual key provided via --ssh-pubkey flag
if [[ -f "$auth_keys" ]] && grep -qF "$SSH_PUBKEY" "$auth_keys" 2>/dev/null; then
info "SSH key already present in authorized_keys"
else
echo "$SSH_PUBKEY" >> "$auth_keys"
success "SSH public key added"
fi
else
# No manual key — the daemon will auto-deploy it on first registration
info "SSH key will be deployed automatically when the daemon connects to Bambuddy"
touch "$auth_keys"
fi
chmod 600 "$auth_keys"
chown -R "$SPOOLBUDDY_SERVICE_USER:$SPOOLBUDDY_SERVICE_USER" "$ssh_dir"
}
create_spoolbuddy_service() {
info "Creating SpoolBuddy systemd service..."
@@ -655,8 +868,29 @@ setup_kiosk() {
info "Kiosk user: $KIOSK_USER (home: $KIOSK_HOME)"
info "Kiosk URL: $KIOSK_URL"
# Allow kiosk user to read SpoolBuddy env so launcher can resolve backend URL
# and API key dynamically instead of using stale install-time fallback values.
local spoolbuddy_env="$INSTALL_PATH/spoolbuddy/.env"
if [[ -f "$spoolbuddy_env" ]]; then
usermod -aG "$SPOOLBUDDY_SERVICE_USER" "$KIOSK_USER" 2>/dev/null || true
chgrp "$SPOOLBUDDY_SERVICE_USER" "$spoolbuddy_env" 2>/dev/null || true
chmod 640 "$spoolbuddy_env" 2>/dev/null || true
fi
# ── Install kiosk packages ────────────────────────────────────────────
# Temporarily block initramfs rebuilds during package install — we rebuild
# once at the end after the Plymouth theme is configured, saving ~4 runs
# (one per installed kernel per hook trigger).
if [[ -x /usr/sbin/update-initramfs ]]; then
dpkg-divert --local --rename --add /usr/sbin/update-initramfs >/dev/null 2>&1 || true
ln -sf /bin/true /usr/sbin/update-initramfs
fi
run_with_progress "Installing kiosk packages" apt-get install -y labwc chromium plymouth wlr-randr
# Restore real update-initramfs
if dpkg-divert --list /usr/sbin/update-initramfs 2>/dev/null | grep -q local; then
rm -f /usr/sbin/update-initramfs
dpkg-divert --local --rename --remove /usr/sbin/update-initramfs >/dev/null 2>&1 || true
fi
# ── config.txt tweaks ─────────────────────────────────────────────────
local boot_config="/boot/firmware/config.txt"
@@ -765,6 +999,10 @@ EOF
mkdir -p /etc/systemd/system/getty@tty1.service.d
cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
[Unit]
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin $KIOSK_USER --noclear %I \$TERM
@@ -809,18 +1047,61 @@ EOF
</labwc_config>
EOF
# ── labwc autostart ───────────────────────────────────────────────────
cat > "$labwc_dir/autostart" << EOF
# ── kiosk launcher (dynamic URL from spoolbuddy/.env) ─────────────────
local kiosk_launcher="/usr/local/bin/spoolbuddy-kiosk-launch"
cat > "$kiosk_launcher" << EOF
#!/usr/bin/env bash
set -euo pipefail
ENV_FILE="$INSTALL_PATH/spoolbuddy/.env"
FALLBACK_URL="$KIOSK_URL"
backend_url=""
api_key=""
if [[ -r "\$ENV_FILE" ]]; then
backend_url="\$(sed -n 's/^SPOOLBUDDY_BACKEND_URL=//p' "\$ENV_FILE" | tail -n1 | tr -d '\r')"
api_key="\$(sed -n 's/^SPOOLBUDDY_API_KEY=//p' "\$ENV_FILE" | tail -n1 | tr -d '\r')"
backend_url="\${backend_url%\"}"
backend_url="\${backend_url#\"}"
api_key="\${api_key%\"}"
api_key="\${api_key#\"}"
elif [[ -f "\$ENV_FILE" ]]; then
echo "spoolbuddy-kiosk-launch: ERROR: \$ENV_FILE exists but is not readable" >&2
echo "spoolbuddy-kiosk-launch: Fix permissions (group-readable by kiosk user) and restart kiosk" >&2
exit 1
fi
if [[ -n "\$backend_url" && -n "\$api_key" ]]; then
backend_url="\${backend_url%/}"
kiosk_url="\${backend_url}/spoolbuddy?token=\${api_key}"
else
kiosk_url="\$FALLBACK_URL"
fi
exec chromium --kiosk --no-first-run --disable-infobars \
--disable-session-crashed-bubble --disable-features=TranslateUI \
--noerrdialogs --disable-component-update \
--overscroll-history-navigation=0 \
--ozone-platform=wayland \
"\$kiosk_url"
EOF
chmod 755 "$kiosk_launcher"
# Tiny self-check: ensure sed command substitutions were not expanded
# while generating the launcher script.
if ! grep -Fq 'backend_url="$(sed -n' "$kiosk_launcher" || ! grep -Fq 'api_key="$(sed -n' "$kiosk_launcher"; then
error "Kiosk launcher generation failed: dynamic env parsing commands were expanded unexpectedly"
fi
# ── labwc autostart ───────────────────────────────────────────────────
cat > "$labwc_dir/autostart" << EOF
# Force 1024x600 (panel doesn't advertise this natively)
wlr-randr --output HDMI-A-1 --custom-mode 1024x600@60 &
# Launch Chromium in kiosk mode (virtual keyboard is embedded in the web app)
chromium --kiosk --no-first-run --disable-infobars \\
--disable-session-crashed-bubble --disable-features=TranslateUI \\
--noerrdialogs --disable-component-update \\
--overscroll-history-navigation=0 \\
--ozone-platform=wayland \\
$KIOSK_URL &
# Launch Chromium via helper that resolves URL from spoolbuddy/.env
$kiosk_launcher &
EOF
chown -R "$KIOSK_USER:$KIOSK_USER" "$labwc_dir"
@@ -855,6 +1136,14 @@ parse_args() {
INSTALL_MODE="$2"
shift 2
;;
--repo)
INSTALL_REPO="$(normalize_github_repo_url "$2")"
shift 2
;;
--ref)
INSTALL_REF="$2"
shift 2
;;
--bambuddy-url)
BAMBUDDY_URL="$2"
shift 2
@@ -871,6 +1160,10 @@ parse_args() {
BAMBUDDY_PORT="$2"
shift 2
;;
--ssh-pubkey)
SSH_PUBKEY="$2"
shift 2
;;
--yes|-y)
NON_INTERACTIVE="true"
shift
@@ -927,6 +1220,47 @@ gather_config() {
fi
prompt "Installation directory" "$INSTALL_PATH" INSTALL_PATH
if [[ -z "$INSTALL_REPO" ]]; then
INSTALL_REPO="$GITHUB_REPO"
fi
prompt "Git repository URL" "$INSTALL_REPO" INSTALL_REPO
INSTALL_REPO="$(normalize_github_repo_url "$INSTALL_REPO")"
if [[ -z "$INSTALL_REF" ]]; then
INSTALL_REF="main"
fi
if [[ "$NON_INTERACTIVE" != "true" && -n "$DETECTED_INSTALLER_REF" && "$DETECTED_INSTALLER_REF" != "HEAD" ]]; then
echo ""
echo -e "${BOLD}Install Source Ref${NC}"
echo "1) main"
echo "2) $DETECTED_INSTALLER_REF (detected from installer context)"
echo "3) custom"
while true; do
echo -en "${BOLD}Choose${NC} [1/2/3]: "
read -r ref_choice
case "$ref_choice" in
""|1)
INSTALL_REF="main"
break
;;
2)
INSTALL_REF="$DETECTED_INSTALLER_REF"
break
;;
3)
prompt "Git ref (branch/tag/commit)" "$INSTALL_REF" INSTALL_REF
break
;;
*)
echo "Please enter 1, 2, or 3."
;;
esac
done
else
prompt "Git ref (branch/tag/commit)" "$INSTALL_REF" INSTALL_REF
fi
if [[ "$INSTALL_MODE" == "spoolbuddy" ]]; then
# Need remote Bambuddy URL and API key
echo ""
@@ -964,6 +1298,8 @@ gather_config() {
echo -e "${CYAN}─────────────────────────────────────────${NC}"
echo -e " Mode: ${GREEN}$([ "$INSTALL_MODE" == "full" ] && echo "Bambuddy + SpoolBuddy" || echo "SpoolBuddy only")${NC}"
echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
echo -e " Git repo: ${GREEN}$INSTALL_REPO${NC}"
echo -e " Git ref: ${GREEN}$INSTALL_REF${NC}"
if [[ "$INSTALL_MODE" == "full" ]]; then
echo -e " Bambuddy port: ${GREEN}$BAMBUDDY_PORT${NC}"
echo -e " Bambuddy URL: ${GREEN}$BAMBUDDY_URL${NC}"
@@ -984,6 +1320,7 @@ gather_config() {
main() {
parse_args "$@"
detect_installer_source_context
echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
@@ -1037,6 +1374,8 @@ main() {
# ── Step 2: System packages ───────────────────────────────────────────
install_system_packages
install_wifi_safeguard
upgrade_system_packages
detect_python || error "Failed to install Python 3.10+"
echo ""
@@ -1058,6 +1397,11 @@ main() {
info "Setting up SpoolBuddy..."
setup_spoolbuddy_venv
create_spoolbuddy_env
# Kiosk env access: only needed if actual kiosk hardware is available
if [[ -f /boot/firmware/config.txt ]] || [[ -f /boot/config.txt ]]; then
ensure_kiosk_env_access
fi
setup_ssh_key
create_spoolbuddy_service
echo ""
Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 118 KiB