diff --git a/emmc-provisioning/README.md b/emmc-provisioning/README.md index 922c404..cc54ad2 100644 --- a/emmc-provisioning/README.md +++ b/emmc-provisioning/README.md @@ -35,7 +35,8 @@ emmc-provisioning/ │ ├── user-data-remote-gnss.example Example cloud-init user-data (curl first-boot.sh) │ ├── config-files/ LightDM, Maliit, one-shots (.desktop + scripts) │ │ ├── chromium-kiosk.desktop -│ │ ├── set-rotation-once.desktop +│ │ ├── 01-set-rotation-once.desktop +│ │ ├── 02-set-wallpaper-once.desktop │ │ └── ... │ ├── files-from-guard/ Plymouth, splash assets; README of required files │ └── fix-reterminal-display.sh One-time fix script (splash, rotation, wallpaper) diff --git a/emmc-provisioning/cloud-init/set-rotation-once.sh b/emmc-provisioning/cloud-init/01-set-rotation-once.sh similarity index 82% rename from emmc-provisioning/cloud-init/set-rotation-once.sh rename to emmc-provisioning/cloud-init/01-set-rotation-once.sh index 2decf9d..8da3be8 100644 --- a/emmc-provisioning/cloud-init/set-rotation-once.sh +++ b/emmc-provisioning/cloud-init/01-set-rotation-once.sh @@ -3,7 +3,8 @@ # Runs once as user pi at first login; deletes its autostart and this script so it never runs again. # Logs to /var/log/first-boot.log. FIRST_BOOT_LOG="/var/log/first-boot.log" -log() { echo "[$(date -Iseconds)] [set-rotation-once] $*" >> "$FIRST_BOOT_LOG" 2>/dev/null || true; } +BASE="$(basename "$0" .sh)" +log() { echo "[$(date -Iseconds)] [$BASE] $*" >> "$FIRST_BOOT_LOG" 2>/dev/null || true; } log "started (labwc/wlr-randr)" log "waiting 5s for compositor ..." @@ -26,5 +27,5 @@ else fi log "removing one-shot desktop and script" -rm -f /home/pi/.config/autostart/set-rotation-once.desktop /home/pi/set-rotation-once.sh +rm -f "$HOME/.config/autostart/${BASE}.desktop" "$HOME/${BASE}.sh" log "finished" diff --git a/emmc-provisioning/cloud-init/set-wallpaper-once.sh b/emmc-provisioning/cloud-init/02-set-wallpaper-once.sh similarity index 84% rename from emmc-provisioning/cloud-init/set-wallpaper-once.sh rename to emmc-provisioning/cloud-init/02-set-wallpaper-once.sh index ae39c4f..4c09c24 100644 --- a/emmc-provisioning/cloud-init/set-wallpaper-once.sh +++ b/emmc-provisioning/cloud-init/02-set-wallpaper-once.sh @@ -2,7 +2,8 @@ # One-shot: set desktop wallpaper for labwc (swaybg) and persist via labwc autostart, then remove self. # Runs as user pi at first login. Logs to /var/log/first-boot.log. FIRST_BOOT_LOG="/var/log/first-boot.log" -log() { echo "[$(date -Iseconds)] [set-wallpaper-once] $*" >> "$FIRST_BOOT_LOG" 2>/dev/null || true; } +BASE="$(basename "$0" .sh)" +log() { echo "[$(date -Iseconds)] [$BASE] $*" >> "$FIRST_BOOT_LOG" 2>/dev/null || true; } WALLPAPER="/usr/share/rpd-wallpaper/splash.png" LABWC_AUTOSTART="$HOME/.config/labwc/autostart" @@ -32,5 +33,5 @@ else fi log "removing one-shot desktop and script" -rm -f /home/pi/.config/autostart/set-wallpaper-once.desktop /home/pi/set-wallpaper-once.sh +rm -f "$HOME/.config/autostart/${BASE}.desktop" "$HOME/${BASE}.sh" log "finished" diff --git a/emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop b/emmc-provisioning/cloud-init/config-files/01-set-rotation-once.desktop similarity index 50% rename from emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop rename to emmc-provisioning/cloud-init/config-files/01-set-rotation-once.desktop index d30ae0b..afa3c68 100644 --- a/emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop +++ b/emmc-provisioning/cloud-init/config-files/01-set-rotation-once.desktop @@ -1,5 +1,5 @@ [Desktop Entry] Type=Application -Name=Set rotation once -Exec=/home/pi/set-rotation-once.sh +Name=01 Set rotation once +Exec=/home/pi/01-set-rotation-once.sh X-GNOME-Autostart-enabled=true diff --git a/emmc-provisioning/cloud-init/config-files/02-set-wallpaper-once.desktop b/emmc-provisioning/cloud-init/config-files/02-set-wallpaper-once.desktop new file mode 100644 index 0000000..bd6d23e --- /dev/null +++ b/emmc-provisioning/cloud-init/config-files/02-set-wallpaper-once.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=02 Set wallpaper once +Exec=/home/pi/02-set-wallpaper-once.sh +X-GNOME-Autostart-enabled=true diff --git a/emmc-provisioning/cloud-init/config-files/README.md b/emmc-provisioning/cloud-init/config-files/README.md index 9b5d996..e02ec04 100644 --- a/emmc-provisioning/cloud-init/config-files/README.md +++ b/emmc-provisioning/cloud-init/config-files/README.md @@ -8,6 +8,5 @@ first-boot.sh downloads these from `FILE_SERVER` (e.g. `http://10.20.50.1:5000/f | 99-wallpaper.conf | /etc/lightdm/lightdm.conf.d/99-wallpaper.conf | | 99-default-session.conf | /etc/lightdm/lightdm.conf.d/99-default-session.conf (rpd-labwc) | | maliit-keyboard.desktop | /home/pi/.config/autostart/maliit-keyboard.desktop | -| set-rotation-once.desktop | /home/pi/.config/autostart/set-rotation-once.desktop (with set-rotation-once.sh) | - -Wallpaper is set once during first-boot via pcmanfm config; no set-wallpaper-once one-shot. +| 01-set-rotation-once.desktop | /home/pi/.config/autostart/01-set-rotation-once.desktop (with 01-set-rotation-once.sh) | +| 02-set-wallpaper-once.desktop | /home/pi/.config/autostart/02-set-wallpaper-once.desktop (with 02-set-wallpaper-once.sh). Wallpaper is also set during first-boot via pcmanfm. | diff --git a/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop b/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop deleted file mode 100644 index 170f4a8..0000000 --- a/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop +++ /dev/null @@ -1,5 +0,0 @@ -[Desktop Entry] -Type=Application -Name=Set wallpaper once -Exec=/home/pi/set-wallpaper-once.sh -X-GNOME-Autostart-enabled=true diff --git a/emmc-provisioning/cloud-init/files-from-guard/README.md b/emmc-provisioning/cloud-init/files-from-guard/README.md index d4ed042..1a5276b 100644 --- a/emmc-provisioning/cloud-init/files-from-guard/README.md +++ b/emmc-provisioning/cloud-init/files-from-guard/README.md @@ -14,9 +14,9 @@ first-boot.sh downloads from **`.../files/first-boot/`** (e.g. `http://10.20.50. | **99-wallpaper.conf** | LightDM greeter wallpaper (from `config-files/`). | | **99-default-session.conf** | LightDM default session rpd-labwc (from `config-files/`). | | **maliit-keyboard.desktop** | Maliit on-screen keyboard autostart (from `config-files/`). | -| **set-rotation-once.sh** + **.desktop** | One-shot: wlr-randr rotation (Left) at first login. | +| **01-set-rotation-once.sh** + **.desktop** | One-shot: wlr-randr rotation (Left) at first login. | -Desktop wallpaper is set once during first-boot via pcmanfm config (first-boot.sh); no set-wallpaper-once one-shot needed. +Desktop wallpaper is set once during first-boot via pcmanfm config (first-boot.sh). Optional one-shot: **02-set-wallpaper-once.sh**. --- @@ -27,4 +27,4 @@ Desktop wallpaper is set once during first-boot via pcmanfm config (first-boot.s - **plymouth-custom/custom.script** — Plymouth script that draws splash.png; host as `custom.script`. - **lightdm/RPiSystem_dark.png** — Unused; only `splash.png` is used now. - **start-chromium.sh** and **chromium-kiosk.desktop** live in `cloud-init/` and `cloud-init/config-files/`; `scripts/sync-portal-files-to-lxc.sh` copies them to the portal first-boot folder. -- **set-rotation-once.sh** (and its .desktop) are in `cloud-init/` and synced to `portal-files/first-boot/` by the same script. +- **01-set-rotation-once.sh**, **02-set-wallpaper-once.sh** (and their .desktop files) are in `cloud-init/` and synced to `portal-files/first-boot/` by the same script. diff --git a/emmc-provisioning/cloud-init/first-boot.conf b/emmc-provisioning/cloud-init/first-boot.conf new file mode 100644 index 0000000..b1087d8 --- /dev/null +++ b/emmc-provisioning/cloud-init/first-boot.conf @@ -0,0 +1,60 @@ +# first-boot.conf.example +# Copy to first-boot.conf and edit. Loaded by first-boot.sh from: +# - same directory as first-boot.sh, or +# - /tmp/first-boot.conf (when run via cloud-init), or +# - /etc/cm4-provisioning/first-boot.conf +# Unset variables keep the script's built-in defaults. + +# --- File server & host --- +# Base URL for first-boot assets (scripts, splash, configs). No trailing slash. +FILE_SERVER="http://10.20.50.1:5000/files/first-boot" + +# Hostname set on the device. +HOSTNAME="guard" + +# --- User --- +# Login user (must exist; cloud-init user-data must create the same user). +PI_USER="pi" + +# --- Paths (optional overrides) --- +# First-boot log file. +LOGFILE="/var/log/first-boot.log" + +# Plymouth custom theme directory. +PLYMOUTH_DIR="/usr/share/plymouth/themes/custom" + +# Wallpaper path (splash.png is also copied here). +WALLPAPER_PATH="/usr/share/rpd-wallpaper/splash.png" + +# --- Packages --- +# Space-separated list of packages to install. Must include: git chromium wmctrl openssh-server +# swaybg wlr-randr maliit-keyboard xinput-calibrator (for kiosk + labwc + touch). +PACKAGES="git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator" + +# --- Desktop & theme --- +# LightDM session (rpd-labwc for Wayland + labwc). +LIGHTDM_SESSION="rpd-labwc" + +# GTK dark theme name (e.g. PiXnoir; fallback is Adwaita-dark if missing). +GTK_THEME_NAME="PiXnoir" + +# Wallpaper mode for pcmanfm (crop, stretch, fit, center, tile, screen, color). +WALLPAPER_MODE="crop" + +# --- Display (reTerminal DM) --- +# Kernel cmdline: DSI rotation. 90 = 90° clockwise; use 180 or 270 for other orientations. +DSI_ROTATE="270" + +# Kernel cmdline: swiotlb size (for vc4-drm/DSI). Leave empty to skip. +SWIOTLB_SIZE="65536" + +# --- reTerminal (Seeed) --- +# Device passed to reTerminal.sh (reTerminal-DM for reTerminal DM). +RETERMINAL_DEVICE="reTerminal-DM" + +# Seeed overlays repo (clone URL). Leave empty to skip driver install. +RETERMINAL_REPO_URL="https://github.com/Seeed-Studio/seeed-linux-dtoverlays" + +# --- One-shots --- +# Space-separated names of one-shot scripts (numbered = run order at first login). Leave empty for none. +ONESHOT_SCRIPTS="01-set-rotation-once 02-set-wallpaper-once" diff --git a/emmc-provisioning/cloud-init/first-boot.conf.example b/emmc-provisioning/cloud-init/first-boot.conf.example index 088406c..2d011ca 100644 --- a/emmc-provisioning/cloud-init/first-boot.conf.example +++ b/emmc-provisioning/cloud-init/first-boot.conf.example @@ -57,5 +57,12 @@ # --- One-shots --- # Space-separated names of one-shot scripts to install from FILE_SERVER (each name gets name.sh + name.desktop). -# Example: "set-rotation-once set-wallpaper-once". Leave empty for none. -# ONESHOT_SCRIPTS="" +# Use numbered names so they run in order at first login. Leave empty for none. +# Example: "01-set-rotation-once 02-set-wallpaper-once" +# ONESHOT_SCRIPTS="01-set-rotation-once 02-set-wallpaper-once" + +# --- Step enable flags (1 = run, 0 = skip). Default: all 1. Set in config to disable a step. --- +# 01=hostname, 02=packages, 03=kiosk_files, 04=splash_wallpaper, 05=lightdm, 06=maliit, +# 07=dark_theme, 08=reterminal_drivers, 09=reapply_splash, 10=cmdline, 11=oneshots, 12=log_permissions, 13=reboot +# ENABLE_STEP_08=0 +# ENABLE_STEP_13=0 diff --git a/emmc-provisioning/cloud-init/first-boot.md b/emmc-provisioning/cloud-init/first-boot.md index fd4118c..80e7edb 100644 --- a/emmc-provisioning/cloud-init/first-boot.md +++ b/emmc-provisioning/cloud-init/first-boot.md @@ -16,7 +16,9 @@ Only set variables you want to change; the rest use built-in defaults. See `firs --- -## Structure (sections) +## Structure (steps and enable flags) + +The script runs **13 numbered steps** (`step_01_hostname` … `step_13_reboot`). Disable a step by setting `ENABLE_STEP_NN=0` in config (default: all `1`). Step table: 01=hostname, 02=packages, 03=kiosk_files, 04=splash_wallpaper, 05=lightdm, 06=maliit, 07=dark_theme, 08=reterminal_drivers, 09=reapply_splash, 10=cmdline, 11=oneshots, 12=log_permissions, 13=reboot. 1. **Config & constants** — Load optional `first-boot.conf`; then `FILE_SERVER`, `PI_USER`, paths, log file (defaults or from config). 2. **Logging** — All output tee’d to `/var/log/first-boot.log`. @@ -29,8 +31,6 @@ Only set variables you want to change; the rest use built-in defaults. See `firs 9. **reTerminal DM drivers** — Seeed repo clone and `reTerminal.sh`. 10. **Re-apply splash** — Set `disable_splash=0`, Plymouth theme to `custom` only, `update-initramfs`. 11. **Dark theme** — Set GTK dark theme for user `pi`: `~/.config/gtk-3.0/settings.ini` with `gtk-application-prefer-dark-theme=1` and `gtk-theme-name=PiXnoir` (Raspberry Pi OS dark theme). -12. **One-shots** — Download `set-rotation-once.sh` + `.desktop` from file server (wlr-randr for labwc). Wallpaper is set once via pcmanfm config during first-boot. -13. **Reboot.** --- diff --git a/emmc-provisioning/cloud-init/first-boot.sh b/emmc-provisioning/cloud-init/first-boot.sh index 8a6b34c..5fd6971 100644 --- a/emmc-provisioning/cloud-init/first-boot.sh +++ b/emmc-provisioning/cloud-init/first-boot.sh @@ -3,6 +3,7 @@ # Run by cloud-init (user-data-remote-gnss.example). Run as root. # Optional: copy first-boot.conf.example to first-boot.conf and edit; it is loaded # from the script dir, /tmp/first-boot.conf, or /etc/cm4-provisioning/first-boot.conf. +# Set ENABLE_STEP_01=0 .. ENABLE_STEP_13=0 in config to disable a step (default: all enabled). set -e export DEBIAN_FRONTEND=noninteractive @@ -24,6 +25,20 @@ SWIOTLB_SIZE="${SWIOTLB_SIZE:-65536}" RETERMINAL_DEVICE="${RETERMINAL_DEVICE:-reTerminal-DM}" RETERMINAL_REPO_URL="${RETERMINAL_REPO_URL:-https://github.com/Seeed-Studio/seeed-linux-dtoverlays}" ONESHOT_SCRIPTS="${ONESHOT_SCRIPTS:-}" +# Step enable flags (1 = run, 0 = skip). Default all enabled. +ENABLE_STEP_01="${ENABLE_STEP_01:-1}" +ENABLE_STEP_02="${ENABLE_STEP_02:-1}" +ENABLE_STEP_03="${ENABLE_STEP_03:-1}" +ENABLE_STEP_04="${ENABLE_STEP_04:-1}" +ENABLE_STEP_05="${ENABLE_STEP_05:-1}" +ENABLE_STEP_06="${ENABLE_STEP_06:-1}" +ENABLE_STEP_07="${ENABLE_STEP_07:-1}" +ENABLE_STEP_08="${ENABLE_STEP_08:-1}" +ENABLE_STEP_09="${ENABLE_STEP_09:-1}" +ENABLE_STEP_10="${ENABLE_STEP_10:-1}" +ENABLE_STEP_11="${ENABLE_STEP_11:-1}" +ENABLE_STEP_12="${ENABLE_STEP_12:-1}" +ENABLE_STEP_13="${ENABLE_STEP_13:-1}" # --- Load config file (first found) --- FIRST_BOOT_CONF="" @@ -40,6 +55,13 @@ done PI_HOME="/home/$PI_USER" AUTOSTART="$PI_HOME/.config/autostart" +# Portal base URL for first-boot status API (derived from FILE_SERVER, e.g. http://10.20.50.1:5000) +if [[ "$FILE_SERVER" =~ ^(https?://[^/]+) ]]; then + PORTAL_BASE="${BASH_REMATCH[1]}" +else + PORTAL_BASE="" +fi + # --- Logging --- log() { echo "[$(date -Iseconds)] $*"; } exec > >(tee -a "$LOGFILE") 2>&1 @@ -47,22 +69,31 @@ log "=== first-boot.sh started ===" [[ -n "$FIRST_BOOT_CONF" ]] && log "Config loaded from $FIRST_BOOT_CONF" || log "Using built-in defaults (no config file found)" log "FILE_SERVER=$FILE_SERVER PI_USER=$PI_USER HOSTNAME=$HOSTNAME LOGFILE=$LOGFILE" -# --- 0. Hostname and /etc/hosts (avoids "unable to resolve host" with sudo) --- -log "--- Hostname: $HOSTNAME ---" -echo "$HOSTNAME" > /etc/hostname -hostnamectl set-hostname "$HOSTNAME" 2>/dev/null || true -# Ensure hostname resolves so sudo and other tools don't warn -if ! grep -q "127.0.1.1[[:space:]]*$HOSTNAME" /etc/hosts 2>/dev/null; then - sed -i "/127.0.1.1[[:space:]].*$/d" /etc/hosts - echo "127.0.1.1 $HOSTNAME" >> /etc/hosts -fi -log "Hostname set to $HOSTNAME; /etc/hosts updated" +# --- Report status to portal (no-op if PORTAL_BASE empty; failures ignored) --- +report_status() { + local phase="$1" message="$2" step="$3" step_name="$4" ip="$5" + [[ -z "$PORTAL_BASE" ]] && return 0 + local json="{\"phase\":\"${phase}\",\"message\":\"${message}\",\"step\":\"${step}\",\"step_name\":\"${step_name}\",\"hostname\":\"${HOSTNAME}\",\"ip\":\"${ip}\"}" + curl -s -X POST -H "Content-Type: application/json" -d "$json" "${PORTAL_BASE}/api/first-boot-status" || true +} +report_status "started" "First-boot started" "" "" "" -# --- Helpers --- -# Download script + .desktop from FILE_SERVER and install as one-shot autostart (runs once at pi's first login, then deletes itself). +# --- Helper: run step if enabled --- +run_step() { + local n="$1" name="$2" enable_var="ENABLE_STEP_${n}" + if [[ "${!enable_var}" == "1" ]]; then + log "--- Step $n: $name ---" + "step_${n}_${name}" || return $? + report_status "running" "Step $n: $name completed" "$n" "$name" "" + else + log "--- Step $n: $name (disabled) ---" + fi +} + +# --- Helper: install one-shot from FILE_SERVER --- install_oneshot() { local name="$1" - log "--- Installing one-shot: $name ---" + log "Installing one-shot: $name" if curl -fsSL "${FILE_SERVER}/${name}.sh" -o "$PI_HOME/${name}.sh"; then log "Downloaded ${name}.sh to $PI_HOME/${name}.sh" else @@ -78,170 +109,205 @@ install_oneshot() { log "One-shot $name installed (will run at first login and then remove itself)" } -# --- 1. Packages --- -log "--- Installing packages ---" -log "Running apt-get update ..." -apt-get update -qq -log "Installing: $PACKAGES" -apt-get install -y -qq $PACKAGES -log "Packages installed successfully" - -# --- 2. Dirs and kiosk files from file server --- -log "--- Kiosk files ---" -log "Creating $AUTOSTART" -mkdir -p "$AUTOSTART" -log "Downloading start-chromium.sh from ${FILE_SERVER}/start-chromium.sh" -curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o "$PI_HOME/start-chromium.sh" -log "Downloading chromium-kiosk.desktop from ${FILE_SERVER}/chromium-kiosk.desktop" -curl -fsSL "${FILE_SERVER}/chromium-kiosk.desktop" -o "$AUTOSTART/chromium-kiosk.desktop" -chmod 755 "$PI_HOME/start-chromium.sh" && chmod 644 "$AUTOSTART/chromium-kiosk.desktop" -chown -R "$PI_USER:$PI_USER" "$PI_HOME/start-chromium.sh" "$AUTOSTART/chromium-kiosk.desktop" -log "Kiosk files installed under $PI_HOME and $AUTOSTART" - -# --- 3. Boot splash and wallpaper (splash.png + Plymouth theme from file server) --- -log "--- Boot splash and wallpaper ---" -log "Creating $PLYMOUTH_DIR and /usr/share/rpd-wallpaper" -mkdir -p "$PLYMOUTH_DIR" /usr/share/rpd-wallpaper -if curl -fsSL "${FILE_SERVER}/splash.png" -o "$PLYMOUTH_DIR/splash.png"; then - log "Downloaded splash.png; copying to $WALLPAPER_PATH" - cp "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH" - chmod 644 "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH" - if curl -fsSL "${FILE_SERVER}/custom.plymouth" -o "$PLYMOUTH_DIR/custom.plymouth" \ - && curl -fsSL "${FILE_SERVER}/custom.script" -o "$PLYMOUTH_DIR/custom.script"; then - chmod 644 "$PLYMOUTH_DIR/custom.plymouth" "$PLYMOUTH_DIR/custom.script" - log "Plymouth theme files (custom.plymouth, custom.script) installed" - else - log "WARNING: Could not download custom.plymouth/custom.script; boot splash theme may be incomplete" +# --- Step 01: Hostname and /etc/hosts --- +step_01_hostname() { + echo "$HOSTNAME" > /etc/hostname + hostnamectl set-hostname "$HOSTNAME" 2>/dev/null || true + if ! grep -q "127.0.1.1[[:space:]]*$HOSTNAME" /etc/hosts 2>/dev/null; then + sed -i "/127.0.1.1[[:space:]].*$/d" /etc/hosts + echo "127.0.1.1 $HOSTNAME" >> /etc/hosts fi - grep -q '^Theme=custom' /etc/plymouth/plymouthd.conf 2>/dev/null || printf '%s\n' '[Daemon]' 'Theme=custom' >> /etc/plymouth/plymouthd.conf - log "Running update-initramfs (may take a moment) ..." - update-initramfs -u -k all 2>/dev/null || true - mkdir -p /etc/lightdm/lightdm.conf.d - curl -fsSL "${FILE_SERVER}/99-wallpaper.conf" -o /etc/lightdm/lightdm.conf.d/99-wallpaper.conf 2>/dev/null || log "WARNING: Could not download 99-wallpaper.conf" - # Set desktop wallpaper once via pcmanfm config (rpd-labwc uses pcmanfm-pi; profile LXDE-pi or default) - for PROFILE in LXDE-pi default; do - PCMANFM_DESKTOP="$PI_HOME/.config/pcmanfm/$PROFILE/desktop-items-0.conf" - mkdir -p "$(dirname "$PCMANFM_DESKTOP")" - if [[ ! -f "$PCMANFM_DESKTOP" ]]; then - printf '%s\n' '[*]' "wallpaper=$WALLPAPER_PATH" "wallpaper_mode=$WALLPAPER_MODE" 'wallpaper_common=1' > "$PCMANFM_DESKTOP" + log "Hostname set to $HOSTNAME; /etc/hosts updated" +} + +# --- Step 02: Packages --- +step_02_packages() { + log "Running apt-get update ..." + apt-get update -qq + log "Installing: $PACKAGES" + apt-get install -y -qq $PACKAGES + log "Packages installed successfully" +} + +# --- Step 03: Kiosk files from file server --- +step_03_kiosk_files() { + log "Creating $AUTOSTART" + mkdir -p "$AUTOSTART" + log "Downloading start-chromium.sh from ${FILE_SERVER}/start-chromium.sh" + curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o "$PI_HOME/start-chromium.sh" + log "Downloading chromium-kiosk.desktop from ${FILE_SERVER}/chromium-kiosk.desktop" + curl -fsSL "${FILE_SERVER}/chromium-kiosk.desktop" -o "$AUTOSTART/chromium-kiosk.desktop" + chmod 755 "$PI_HOME/start-chromium.sh" && chmod 644 "$AUTOSTART/chromium-kiosk.desktop" + chown -R "$PI_USER:$PI_USER" "$PI_HOME/start-chromium.sh" "$AUTOSTART/chromium-kiosk.desktop" + log "Kiosk files installed under $PI_HOME and $AUTOSTART" +} + +# --- Step 04: Boot splash and wallpaper --- +step_04_splash_wallpaper() { + log "Creating $PLYMOUTH_DIR and /usr/share/rpd-wallpaper" + mkdir -p "$PLYMOUTH_DIR" /usr/share/rpd-wallpaper + if curl -fsSL "${FILE_SERVER}/splash.png" -o "$PLYMOUTH_DIR/splash.png"; then + log "Downloaded splash.png; copying to $WALLPAPER_PATH" + cp "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH" + chmod 644 "$PLYMOUTH_DIR/splash.png" "$WALLPAPER_PATH" + if curl -fsSL "${FILE_SERVER}/custom.plymouth" -o "$PLYMOUTH_DIR/custom.plymouth" \ + && curl -fsSL "${FILE_SERVER}/custom.script" -o "$PLYMOUTH_DIR/custom.script"; then + chmod 644 "$PLYMOUTH_DIR/custom.plymouth" "$PLYMOUTH_DIR/custom.script" + log "Plymouth theme files (custom.plymouth, custom.script) installed" else - grep -q '^wallpaper=' "$PCMANFM_DESKTOP" && sed -i "s|^wallpaper=.*|wallpaper=$WALLPAPER_PATH|" "$PCMANFM_DESKTOP" || echo "wallpaper=$WALLPAPER_PATH" >> "$PCMANFM_DESKTOP" - grep -q '^wallpaper_mode=' "$PCMANFM_DESKTOP" && sed -i "s/^wallpaper_mode=.*/wallpaper_mode=$WALLPAPER_MODE/" "$PCMANFM_DESKTOP" || echo "wallpaper_mode=$WALLPAPER_MODE" >> "$PCMANFM_DESKTOP" + log "WARNING: Could not download custom.plymouth/custom.script; boot splash theme may be incomplete" fi - chown -R "$PI_USER:$PI_USER" "$(dirname "$PCMANFM_DESKTOP")" - done - log "Set desktop wallpaper via pcmanfm config (LXDE-pi and default)" - log "Splash and wallpaper set from file server" -else - log "WARNING: Could not download splash.png" -fi + grep -q '^Theme=custom' /etc/plymouth/plymouthd.conf 2>/dev/null || printf '%s\n' '[Daemon]' 'Theme=custom' >> /etc/plymouth/plymouthd.conf + log "Running update-initramfs (may take a moment) ..." + update-initramfs -u -k all 2>/dev/null || true + mkdir -p /etc/lightdm/lightdm.conf.d + curl -fsSL "${FILE_SERVER}/99-wallpaper.conf" -o /etc/lightdm/lightdm.conf.d/99-wallpaper.conf 2>/dev/null || log "WARNING: Could not download 99-wallpaper.conf" + for PROFILE in LXDE-pi default; do + PCMANFM_DESKTOP="$PI_HOME/.config/pcmanfm/$PROFILE/desktop-items-0.conf" + mkdir -p "$(dirname "$PCMANFM_DESKTOP")" + if [[ ! -f "$PCMANFM_DESKTOP" ]]; then + printf '%s\n' '[*]' "wallpaper=$WALLPAPER_PATH" "wallpaper_mode=$WALLPAPER_MODE" 'wallpaper_common=1' > "$PCMANFM_DESKTOP" + else + grep -q '^wallpaper=' "$PCMANFM_DESKTOP" && sed -i "s|^wallpaper=.*|wallpaper=$WALLPAPER_PATH|" "$PCMANFM_DESKTOP" || echo "wallpaper=$WALLPAPER_PATH" >> "$PCMANFM_DESKTOP" + grep -q '^wallpaper_mode=' "$PCMANFM_DESKTOP" && sed -i "s/^wallpaper_mode=.*/wallpaper_mode=$WALLPAPER_MODE/" "$PCMANFM_DESKTOP" || echo "wallpaper_mode=$WALLPAPER_MODE" >> "$PCMANFM_DESKTOP" + fi + chown -R "$PI_USER:$PI_USER" "$(dirname "$PCMANFM_DESKTOP")" + done + log "Set desktop wallpaper via pcmanfm config (LXDE-pi and default)" + log "Splash and wallpaper set from file server" + else + log "WARNING: Could not download splash.png" + fi +} -# --- 4. LightDM: rpd-labwc session + configs from file server --- -log "--- LightDM session (rpd-labwc) ---" -mkdir -p /etc/lightdm/lightdm.conf.d -if curl -fsSL "${FILE_SERVER}/99-default-session.conf" -o /etc/lightdm/lightdm.conf.d/99-default-session.conf 2>/dev/null; then - log "99-default-session.conf installed" -else - log "WARNING: Could not download 99-default-session.conf" -fi -# Raspberry Pi OS may apply main lightdm.conf after .conf.d; force session in main config too -if [[ -f /etc/lightdm/lightdm.conf ]]; then - sed -i "s/^user-session=.*/user-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf - sed -i "s/^autologin-session=.*/autologin-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf - log "Patched /etc/lightdm/lightdm.conf to use $LIGHTDM_SESSION" -fi +# --- Step 05: LightDM session --- +step_05_lightdm() { + mkdir -p /etc/lightdm/lightdm.conf.d + if curl -fsSL "${FILE_SERVER}/99-default-session.conf" -o /etc/lightdm/lightdm.conf.d/99-default-session.conf 2>/dev/null; then + log "99-default-session.conf installed" + else + log "WARNING: Could not download 99-default-session.conf" + fi + if [[ -f /etc/lightdm/lightdm.conf ]]; then + sed -i "s/^user-session=.*/user-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf + sed -i "s/^autologin-session=.*/autologin-session=$LIGHTDM_SESSION/" /etc/lightdm/lightdm.conf + log "Patched /etc/lightdm/lightdm.conf to use $LIGHTDM_SESSION" + fi +} -# --- 5. Maliit on-screen keyboard (from file server) --- -log "--- Maliit ---" -mkdir -p "$AUTOSTART" "$PI_HOME/.config" -curl -fsSL "${FILE_SERVER}/maliit-keyboard.desktop" -o "$AUTOSTART/maliit-keyboard.desktop" 2>/dev/null && log "maliit-keyboard.desktop installed" || log "WARNING: Could not download maliit-keyboard.desktop" +# --- Step 06: Maliit on-screen keyboard --- +step_06_maliit() { + mkdir -p "$AUTOSTART" "$PI_HOME/.config" + curl -fsSL "${FILE_SERVER}/maliit-keyboard.desktop" -o "$AUTOSTART/maliit-keyboard.desktop" 2>/dev/null && log "maliit-keyboard.desktop installed" || log "WARNING: Could not download maliit-keyboard.desktop" +} -# --- 5b. Dark theme (GTK + prefer dark for apps) --- -log "--- Dark theme ---" -GTK_SETTINGS="$PI_HOME/.config/gtk-3.0/settings.ini" -mkdir -p "$(dirname "$GTK_SETTINGS")" -if [[ ! -f "$GTK_SETTINGS" ]]; then - printf '%s\n' '[Settings]' 'gtk-application-prefer-dark-theme=1' "gtk-theme-name=$GTK_THEME_NAME" > "$GTK_SETTINGS" -else - grep -q '^gtk-application-prefer-dark-theme=' "$GTK_SETTINGS" && sed -i 's/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=1/' "$GTK_SETTINGS" || echo 'gtk-application-prefer-dark-theme=1' >> "$GTK_SETTINGS" - grep -q '^gtk-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-theme-name=.*/gtk-theme-name=$GTK_THEME_NAME/" "$GTK_SETTINGS" || echo "gtk-theme-name=$GTK_THEME_NAME" >> "$GTK_SETTINGS" -fi -# Fallback if theme not installed (e.g. older image): Adwaita-dark -log "Set dark theme ($GTK_THEME_NAME) in gtk-3.0/settings.ini" -chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config" +# --- Step 07: Dark theme (GTK) --- +step_07_dark_theme() { + GTK_SETTINGS="$PI_HOME/.config/gtk-3.0/settings.ini" + mkdir -p "$(dirname "$GTK_SETTINGS")" + if [[ ! -f "$GTK_SETTINGS" ]]; then + printf '%s\n' '[Settings]' 'gtk-application-prefer-dark-theme=1' "gtk-theme-name=$GTK_THEME_NAME" > "$GTK_SETTINGS" + else + grep -q '^gtk-application-prefer-dark-theme=' "$GTK_SETTINGS" && sed -i 's/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=1/' "$GTK_SETTINGS" || echo 'gtk-application-prefer-dark-theme=1' >> "$GTK_SETTINGS" + grep -q '^gtk-theme-name=' "$GTK_SETTINGS" && sed -i "s/^gtk-theme-name=.*/gtk-theme-name=$GTK_THEME_NAME/" "$GTK_SETTINGS" || echo "gtk-theme-name=$GTK_THEME_NAME" >> "$GTK_SETTINGS" + fi + log "Set dark theme ($GTK_THEME_NAME) in gtk-3.0/settings.ini" + chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config" +} -# --- 6. reTerminal DM drivers (Seeed) --- -if [[ -n "$RETERMINAL_REPO_URL" ]]; then - log "--- reTerminal DM drivers ---" +# --- Step 08: reTerminal DM drivers (Seeed) --- +step_08_reterminal_drivers() { + if [[ -z "$RETERMINAL_REPO_URL" ]]; then + log "Skipping reTerminal drivers (RETERMINAL_REPO_URL not set)" + return 0 + fi REPO_DIR="/tmp/seeed-linux-dtoverlays" log "Cloning seeed-linux-dtoverlays to $REPO_DIR ..." git clone --depth 1 "$RETERMINAL_REPO_URL" "$REPO_DIR" - # Script must run from repo root (it uses pwd for MOD_PATH). On bookworm+ --compat-kernel is not supported. log "Running reTerminal.sh --device $RETERMINAL_DEVICE from $REPO_DIR ..." if ( cd "$REPO_DIR" && "$REPO_DIR/scripts/reTerminal.sh" --device "$RETERMINAL_DEVICE" ); then log "reTerminal DM drivers installed (reboot will apply)" else log "WARNING: reTerminal.sh failed (see log above). Display/touch may still work; you can retry later with: cd $REPO_DIR && sudo ./scripts/reTerminal.sh --device $RETERMINAL_DEVICE" fi - log "Removing $REPO_DIR" rm -rf "$REPO_DIR" -else - log "--- Skipping reTerminal drivers (RETERMINAL_REPO_URL not set) ---" -fi +} -# --- 6b. Re-apply splash and display (Seeed script sets disable_splash=1 and can duplicate Plymouth theme) --- -log "--- Re-applying boot splash and Plymouth theme ---" -CFG_PATH="/boot/firmware/config.txt" -[[ -f /boot/firmware/config.txt ]] || CFG_PATH="/boot/config.txt" -if [[ -f "$CFG_PATH" ]]; then - if grep -q '^disable_splash=1' "$CFG_PATH"; then +# --- Step 09: Re-apply splash and Plymouth theme --- +step_09_reapply_splash() { + CFG_PATH="/boot/firmware/config.txt" + [[ -f /boot/firmware/config.txt ]] || CFG_PATH="/boot/config.txt" + if [[ -f "$CFG_PATH" ]] && grep -q '^disable_splash=1' "$CFG_PATH"; then sed -i 's/^disable_splash=1$/disable_splash=0/' "$CFG_PATH" log "Set disable_splash=0 so Plymouth splash is shown" fi -fi -# Ensure Plymouth uses our custom theme only (single [Daemon], Theme=custom) -if [[ -f /etc/plymouth/plymouthd.conf ]]; then - sed -i '/^Theme=/d' /etc/plymouth/plymouthd.conf - sed -i '/^\[Daemon\]$/d' /etc/plymouth/plymouthd.conf - grep -q '^\[Daemon\]' /etc/plymouth/plymouthd.conf || echo '[Daemon]' >> /etc/plymouth/plymouthd.conf - echo 'Theme=custom' >> /etc/plymouth/plymouthd.conf - log "Plymouth theme set to custom only" -fi -log "Running update-initramfs to apply Plymouth theme ..." -update-initramfs -u -k all 2>/dev/null || true - -# --- 6b2. Kernel cmdline: swiotlb + DSI rotation (KMS, persistent across reboots) --- -CMDLINE_PATH="/boot/firmware/cmdline.txt" -[[ -f "$CMDLINE_PATH" ]] || CMDLINE_PATH="/boot/cmdline.txt" -if [[ -f "$CMDLINE_PATH" ]]; then - if [[ -n "$SWIOTLB_SIZE" ]] && ! grep -q 'swiotlb=' "$CMDLINE_PATH"; then - sed -i "s/rootwait/rootwait swiotlb=$SWIOTLB_SIZE/" "$CMDLINE_PATH" - log "Added swiotlb=$SWIOTLB_SIZE to kernel cmdline (vc4-drm / DSI)" + if [[ -f /etc/plymouth/plymouthd.conf ]]; then + sed -i '/^Theme=/d' /etc/plymouth/plymouthd.conf + sed -i '/^\[Daemon\]$/d' /etc/plymouth/plymouthd.conf + grep -q '^\[Daemon\]' /etc/plymouth/plymouthd.conf || echo '[Daemon]' >> /etc/plymouth/plymouthd.conf + echo 'Theme=custom' >> /etc/plymouth/plymouthd.conf + log "Plymouth theme set to custom only" fi - # Persistent rotation for DSI-1 (KMS): append at end of single line. 90 = 90° clockwise. - if [[ -n "$DSI_ROTATE" ]] && ! grep -q 'video=DSI-1:rotate=' "$CMDLINE_PATH"; then - sed -i "s/\$/ video=DSI-1:rotate=$DSI_ROTATE/" "$CMDLINE_PATH" - log "Added video=DSI-1:rotate=$DSI_ROTATE to kernel cmdline (DSI rotation)" + log "Running update-initramfs to apply Plymouth theme ..." + update-initramfs -u -k all 2>/dev/null || true +} + +# --- Step 10: Kernel cmdline (swiotlb + DSI rotation) --- +step_10_cmdline() { + CMDLINE_PATH="/boot/firmware/cmdline.txt" + [[ -f "$CMDLINE_PATH" ]] || CMDLINE_PATH="/boot/cmdline.txt" + if [[ -f "$CMDLINE_PATH" ]]; then + if [[ -n "$SWIOTLB_SIZE" ]] && ! grep -q 'swiotlb=' "$CMDLINE_PATH"; then + sed -i "s/rootwait/rootwait swiotlb=$SWIOTLB_SIZE/" "$CMDLINE_PATH" + log "Added swiotlb=$SWIOTLB_SIZE to kernel cmdline (vc4-drm / DSI)" + fi + if [[ -n "$DSI_ROTATE" ]] && ! grep -q 'video=DSI-1:rotate=' "$CMDLINE_PATH"; then + sed -i "s/\$/ video=DSI-1:rotate=$DSI_ROTATE/" "$CMDLINE_PATH" + log "Added video=DSI-1:rotate=$DSI_ROTATE to kernel cmdline (DSI rotation)" + fi fi -fi +} -# --- 7. One-shots (wallpaper already set in pcmanfm config above; rotation is via cmdline.txt) --- -log "--- One-shot scripts (if any) ---" -if [[ -n "$DSI_ROTATE" ]]; then - log "Rotation is set via kernel cmdline (video=DSI-1:rotate=$DSI_ROTATE)" -fi -if [[ -n "$ONESHOT_SCRIPTS" ]]; then - for _name in $ONESHOT_SCRIPTS; do - install_oneshot "$_name" || true - done -else - log "No one-shot scripts configured (ONESHOT_SCRIPTS empty)" -fi +# --- Step 11: One-shot scripts --- +step_11_oneshots() { + if [[ -n "$DSI_ROTATE" ]]; then + log "Rotation is set via kernel cmdline (video=DSI-1:rotate=$DSI_ROTATE)" + fi + if [[ -n "$ONESHOT_SCRIPTS" ]]; then + for _name in $ONESHOT_SCRIPTS; do + install_oneshot "$_name" || true + done + else + log "No one-shot scripts configured (ONESHOT_SCRIPTS empty)" + fi +} -# --- 8. Allow pi to append to first-boot.log (for one-shot scripts) --- -chmod 666 "$LOGFILE" -log "Log file $LOGFILE is now appendable by user $PI_USER for one-shot scripts" +# --- Step 12: Log file permissions --- +step_12_log_permissions() { + chmod 666 "$LOGFILE" + log "Log file $LOGFILE is now appendable by user $PI_USER for one-shot scripts" +} -# --- 9. Reboot --- -log "=== first-boot.sh finished, rebooting ===" -reboot +# --- Step 13: Reboot --- +step_13_reboot() { + DEVICE_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" + report_status "done" "First-boot complete" "13" "reboot" "$DEVICE_IP" + log "Device IP: ${DEVICE_IP:-unknown}" + log "=== first-boot.sh finished, rebooting ===" + reboot +} + +# --- Main: run steps in order --- +run_step 01 hostname +run_step 02 packages +run_step 03 kiosk_files +run_step 04 splash_wallpaper +run_step 05 lightdm +run_step 06 maliit +run_step 07 dark_theme +run_step 08 reterminal_drivers +run_step 09 reapply_splash +run_step 10 cmdline +run_step 11 oneshots +run_step 12 log_permissions +run_step 13 reboot diff --git a/emmc-provisioning/dashboard/app.py b/emmc-provisioning/dashboard/app.py index 6ff9f53..39f2d3a 100644 --- a/emmc-provisioning/dashboard/app.py +++ b/emmc-provisioning/dashboard/app.py @@ -46,6 +46,7 @@ BUILD_STATUS_FILE = Path(os.environ.get("CM4_BUILD_STATUS_FILE", str(BASE_DIR / BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR / "build_cloudinit_request.json"))) SHRINK_REQUEST_FILE = Path(os.environ.get("CM4_SHRINK_REQUEST_FILE", str(BASE_DIR / "shrink_request.json"))) SHRINK_STATUS_FILE = Path(os.environ.get("CM4_SHRINK_STATUS_FILE", str(BASE_DIR / "shrink_status.json"))) +FIRST_BOOT_STATUS_FILE = Path(os.environ.get("CM4_FIRST_BOOT_STATUS_FILE", str(BASE_DIR / "first_boot_status.json"))) CLOUDINIT_TEMPLATES_FILE = Path(os.environ.get("CM4_CLOUDINIT_TEMPLATES_FILE", str(BASE_DIR / "cloudinit_templates.json"))) PORTAL_DESCRIPTIONS_FILE = Path(os.environ.get("CM4_PORTAL_DESCRIPTIONS_FILE", str(BASE_DIR / "portal_descriptions.json"))) DB_PATH = Path(os.environ.get("CM4_DASHBOARD_DB", str(BASE_DIR / "dashboard.db"))) @@ -587,6 +588,59 @@ def api_status_clear(): return jsonify({"ok": False, "error": "Could not write status"}), 500 +def _first_boot_status_read(): + """Read first-boot status (device reports progress during first-boot.sh).""" + try: + with open(FIRST_BOOT_STATUS_FILE, "r") as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {"phase": "idle", "message": "", "step": "", "step_name": "", "hostname": "", "ip": "", "updated": None} + + +def _first_boot_status_write(phase, message="", step="", step_name="", hostname="", ip="", error=None): + """Write first-boot status (called by POST from device).""" + try: + os.makedirs(os.path.dirname(FIRST_BOOT_STATUS_FILE) or ".", exist_ok=True) + data = { + "phase": phase, + "message": message, + "step": step, + "step_name": step_name, + "hostname": hostname, + "ip": ip or "", + "updated": time.time(), + } + if error: + data["error"] = error + with open(FIRST_BOOT_STATUS_FILE, "w") as f: + json.dump(data, f, indent=2) + except (PermissionError, OSError): + pass + + +@app.route("/api/first-boot-status", methods=["GET"]) +def api_first_boot_status_get(): + """Return current first-boot progress (for dashboard to poll).""" + return jsonify(_first_boot_status_read()) + + +@app.route("/api/first-boot-status", methods=["POST"]) +def api_first_boot_status_post(): + """Called by device during first-boot.sh to report progress. No auth (device on local net).""" + body = request.get_json(silent=True) or {} + phase = (body.get("phase") or "running").strip().lower() + if phase not in ("started", "running", "done", "error"): + phase = "running" + message = (body.get("message") or "").strip()[:500] + step = (body.get("step") or "").strip()[:10] + step_name = (body.get("step_name") or "").strip()[:80] + hostname = (body.get("hostname") or "").strip()[:64] + ip = (body.get("ip") or "").strip()[:45] + error = (body.get("error") or "").strip()[:500] if phase == "error" else None + _first_boot_status_write(phase=phase, message=message, step=step, step_name=step_name, hostname=hostname, ip=ip, error=error) + return jsonify({"ok": True}) + + @app.route("/api/log") def api_log(): return jsonify({"log": read_log_tail()}) @@ -653,6 +707,8 @@ def api_device_action(): pass # host may still have SHRINK_BACKUP=1 with open(ACTION_REQUEST_FILE, "w") as f: f.write(action) + if action == "deploy": + _first_boot_status_write("idle", "", hostname="", ip="") return jsonify({"ok": True}) except (PermissionError, OSError): return jsonify({"ok": False, "error": "Could not write action file"}), 500 @@ -669,6 +725,7 @@ def api_device_action(): ip = d.get("ip") or mac if action == "deploy": _write_status("flashing", f"Deploying to {ip} (network)...") + _first_boot_status_write("idle", "", hostname="", ip="") elif action == "backup": _write_status("backup", f"Backing up {ip} (network)...") return jsonify({"ok": True}) diff --git a/emmc-provisioning/dashboard/templates/home.html b/emmc-provisioning/dashboard/templates/home.html index 4c876d3..e220bed 100644 --- a/emmc-provisioning/dashboard/templates/home.html +++ b/emmc-provisioning/dashboard/templates/home.html @@ -108,6 +108,15 @@ + +

Capture or deploy

Network boot (DHCP):

@@ -230,6 +239,28 @@ function fmtSize(n) { if (n >= 1e9) return (n/1e9).toFixed(1)+' GB'; if (n >= 1e6) return (n/1e6).toFixed(1)+' MB'; return (n/1e3).toFixed(0)+' KB'; } function fmtDate(ts) { return new Date(ts*1000).toLocaleString(); } function fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); } + function renderFirstBootStatus(data) { + const phase = data.phase || 'idle'; + const card = document.getElementById('firstBootCard'); + if (phase === 'idle' || phase === '') { card.style.display = 'none'; return; } + card.style.display = 'block'; + const stepEl = document.getElementById('firstBootStep'); + const msgEl = document.getElementById('firstBootMsg'); + const ipWrap = document.getElementById('firstBootIpWrap'); + const ipEl = document.getElementById('firstBootIp'); + stepEl.textContent = data.step ? 'Step ' + data.step : (phase === 'done' ? 'Done' : phase); + stepEl.className = 'status-pill ' + (phase === 'done' ? 'done' : phase === 'error' ? 'error' : 'flashing'); + msgEl.textContent = data.message || (phase === 'done' && data.ip ? 'First-boot finished. Device IP: ' + data.ip : ''); + if (phase === 'done' && data.ip) { + ipWrap.style.display = 'block'; + ipEl.textContent = data.ip; + } else { + ipWrap.style.display = 'none'; + } + } + function fetchFirstBootStatus() { + fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){}); + } function fetchPending() { fetch('/api/pending-devices').then(function(r){ return r.json(); }).then(function(d){ renderPending(d.usb || null, d.network || []); }).catch(function(){ renderPending(null, []); }); } function fetchLog() { fetch('/api/log').then(function(r){ return r.json(); }).then(function(d){ document.getElementById('log').textContent = d.log || ''; }).catch(function(){}); } function fetchGolden() { @@ -286,10 +317,11 @@ .then(function(d){ if(d.ok) fetchDhcpNetboot(); else alert(d.error || 'Failed'); }) .catch(function(){ alert('Request failed'); }); }); - fetchStatus(); fetchLog(); fetchPending(); fetchGolden(); fetchDhcpNetboot(); fetchDhcpLeases(); + fetchStatus(); fetchLog(); fetchPending(); fetchGolden(); fetchDhcpNetboot(); fetchDhcpLeases(); fetchFirstBootStatus(); setInterval(fetchStatus, 2000); setInterval(fetchLog, 4000); setInterval(fetchPending, 2000); + setInterval(fetchFirstBootStatus, 3000); setInterval(fetchGolden, 10000); setInterval(fetchDhcpNetboot, 10000); setInterval(fetchDhcpLeases, 10000); diff --git a/emmc-provisioning/scripts/sync-portal-files-to-lxc.sh b/emmc-provisioning/scripts/sync-portal-files-to-lxc.sh index a2da70d..cdea054 100755 --- a/emmc-provisioning/scripts/sync-portal-files-to-lxc.sh +++ b/emmc-provisioning/scripts/sync-portal-files-to-lxc.sh @@ -2,6 +2,18 @@ # Sync portal (file server) content from the repo to the LXC. # Updates /var/lib/cm4-provisioning/portal-files/ so first-boot and the # dashboard /files/ serve the same scripts and assets as in the repo. +# +# Files first-boot.sh downloads from FILE_SERVER (../files/first-boot/): +# Required: start-chromium.sh, chromium-kiosk.desktop, splash.png, +# custom.plymouth, custom.script, 99-wallpaper.conf, +# 99-default-session.conf, maliit-keyboard.desktop +# One-shots (if ONESHOT_SCRIPTS set): .sh + .desktop +# e.g. 01-set-rotation-once.sh, 01-set-rotation-once.desktop, +# 02-set-wallpaper-once.sh, 02-set-wallpaper-once.desktop +# Optional: first-boot.conf (downloaded to /tmp by cloud-init runcmd) +# Note: splash.png is not in the repo; add it to plymouth-custom/ or upload +# via the portal if you want a custom boot splash. +# # Usage: ./sync-portal-files-to-lxc.sh [user@lxc_ip] # Example: ./sync-portal-files-to-lxc.sh root@10.130.60.141 @@ -22,25 +34,32 @@ echo "Syncing portal files to $LXC ($REMOTE_PORTAL) ..." ssh "$LXC" "command -v rsync >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y rsync)" ssh "$LXC" "mkdir -p $REMOTE_FIRST_BOOT" -# first-boot.sh at portal root (cloud-init downloads it by URL, not from first-boot/ subfolder) +# --- Portal root (URL /files/...) --- +# first-boot.sh: cloud-init runcmd downloads this rsync -avz "$CLOUDINIT_DIR/first-boot.sh" "$LXC:$REMOTE_PORTAL/" +# Optional config: cloud-init can download to /tmp/first-boot.conf before running first-boot.sh +rsync -avz "$CLOUDINIT_DIR/first-boot.conf.example" "$LXC:$REMOTE_PORTAL/" +[[ -f "$CLOUDINIT_DIR/first-boot.conf" ]] && rsync -avz "$CLOUDINIT_DIR/first-boot.conf" "$LXC:$REMOTE_PORTAL/" || true -# config-files/* (includes chromium-kiosk.desktop) → portal-files/first-boot/ +# --- first-boot/ (URL /files/first-boot/...) --- +# Config files: LightDM, Maliit, Chromium kiosk, one-shot .desktop files rsync -avz --exclude='README.md' \ "$CLOUDINIT_DIR/config-files/" \ "$LXC:$REMOTE_FIRST_BOOT/" -# start-chromium.sh → portal-files/first-boot/ -rsync -avz "$CLOUDINIT_DIR/start-chromium.sh" "$LXC:$REMOTE_FIRST_BOOT/" +# Kiosk and scripts +rsync -avz \ + "$CLOUDINIT_DIR/start-chromium.sh" \ + "$CLOUDINIT_DIR/01-set-rotation-once.sh" \ + "$CLOUDINIT_DIR/02-set-wallpaper-once.sh" \ + "$CLOUDINIT_DIR/set-rotation-at-login.sh" \ + "$CLOUDINIT_DIR/fix-reterminal-display.sh" \ + "$LXC:$REMOTE_FIRST_BOOT/" -# plymouth-custom/* (custom.plymouth, custom.script, splash.png if present) → portal-files/first-boot/ +# Plymouth theme (custom.plymouth, custom.script; add splash.png to this dir or upload via portal) rsync -avz \ "$CLOUDINIT_DIR/files-from-guard/plymouth-custom/" \ "$LXC:$REMOTE_FIRST_BOOT/" -# one-shot scripts from cloud-init root → portal-files/first-boot/ (wallpaper set in first-boot via labwc autostart) -rsync -avz \ - "$CLOUDINIT_DIR/set-rotation-once.sh" \ - "$LXC:$REMOTE_FIRST_BOOT/" - echo "Done. Portal files at http://$(echo "$LXC" | cut -d@ -f2):5000/files/" +echo "Note: Add splash.png to $REMOTE_FIRST_BOOT/ (or plymouth-custom/) if you want a custom boot splash."