diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/99-default-session.conf b/chromium-setup/emmc-provisioning/cloud-init/config-files/99-default-session.conf new file mode 100644 index 0000000..1ee4817 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/99-default-session.conf @@ -0,0 +1,2 @@ +[Seat:*] +user-session=plasmax11 diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/99-wallpaper.conf b/chromium-setup/emmc-provisioning/cloud-init/config-files/99-wallpaper.conf new file mode 100644 index 0000000..76bc2b0 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/99-wallpaper.conf @@ -0,0 +1,3 @@ +[greeter] +wallpaper=/usr/share/rpd-wallpaper/splash.png +wallpaper_mode=crop diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/README.md b/chromium-setup/emmc-provisioning/cloud-init/config-files/README.md new file mode 100644 index 0000000..a34fd59 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/README.md @@ -0,0 +1,13 @@ +# Config files for first-boot (file server) + +first-boot.sh downloads these from `FILE_SERVER` (e.g. `http://10.130.60.141:5000/files/first-boot`) and installs them to the paths below. Upload each file into the **first-boot** subfolder of portal-files (e.g. `/var/lib/cm4-provisioning/portal-files/first-boot/`). + +| File on server | Destination on device | +|----------------|------------------------| +| 99-wallpaper.conf | /etc/lightdm/lightdm.conf.d/99-wallpaper.conf | +| 99-default-session.conf | /etc/lightdm/lightdm.conf.d/99-default-session.conf | +| kdeglobals | /home/pi/.config/kdeglobals | +| kwinrc | /home/pi/.config/kwinrc | +| 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) | +| set-wallpaper-once.desktop | /home/pi/.config/autostart/set-wallpaper-once.desktop (with set-wallpaper-once.sh) | diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/kdeglobals b/chromium-setup/emmc-provisioning/cloud-init/config-files/kdeglobals new file mode 100644 index 0000000..3372779 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/kdeglobals @@ -0,0 +1,2 @@ +[General] +ForceFontDPI=120 diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/kwinrc b/chromium-setup/emmc-provisioning/cloud-init/config-files/kwinrc new file mode 100644 index 0000000..bcdb23e --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/kwinrc @@ -0,0 +1,4 @@ +[Windows] +BorderlessMaximizedWindows=true +[Plugins] +touchpointsEnabled=true diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/maliit-keyboard.desktop b/chromium-setup/emmc-provisioning/cloud-init/config-files/maliit-keyboard.desktop new file mode 100644 index 0000000..91436bf --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/maliit-keyboard.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Maliit Keyboard +Exec=maliit-keyboard -r +X-GNOME-Autostart-enabled=true diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop b/chromium-setup/emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop new file mode 100644 index 0000000..d30ae0b --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/set-rotation-once.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Set rotation once +Exec=/home/pi/set-rotation-once.sh +X-GNOME-Autostart-enabled=true diff --git a/chromium-setup/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop b/chromium-setup/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop new file mode 100644 index 0000000..170f4a8 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/config-files/set-wallpaper-once.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Set wallpaper once +Exec=/home/pi/set-wallpaper-once.sh +X-GNOME-Autostart-enabled=true diff --git a/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/README.md b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/README.md new file mode 100644 index 0000000..3fbdc44 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/README.md @@ -0,0 +1,30 @@ +# Files for the file server + +first-boot.sh downloads from **`.../files/first-boot/`** (e.g. `http://10.130.60.141:5000/files/first-boot`). Put all first-boot assets in a **first-boot** subfolder of portal-files so provisioning works. + +## Required files (host under `.../files/first-boot/`) + +| File | Purpose | +|------|--------| +| **start-chromium.sh** | Chromium kiosk launcher (executable). | +| **chromium-kiosk.desktop** | Autostart entry for the kiosk. | +| **splash.png** | Boot splash + login + desktop wallpaper (single image). | +| **custom.plymouth** | Plymouth theme config (from `plymouth-custom/`). | +| **custom.script** | Plymouth script that displays splash.png (from `plymouth-custom/`). | +| **99-wallpaper.conf** | LightDM greeter wallpaper (from `config-files/`). | +| **99-default-session.conf** | LightDM default session plasmax11 (from `config-files/`). | +| **kdeglobals** | KDE font DPI (from `config-files/`). | +| **kwinrc** | KDE window/touch (from `config-files/`). | +| **maliit-keyboard.desktop** | Maliit on-screen keyboard autostart (from `config-files/`). | +| **set-rotation-once.sh** + **.desktop** | One-shot: DSI-1 rotation at first login. | +| **set-wallpaper-once.sh** + **.desktop** | One-shot: desktop wallpaper at first login. | + +--- + +## What’s in this folder + +- **plymouth-custom/splash.png** — Example splash image; host as `splash.png`. +- **plymouth-custom/custom.plymouth** — Plymouth theme definition; host as `custom.plymouth`. +- **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. +- **set-rotation-once.sh** and **set-wallpaper-once.sh** live in `cloud-init/`; copy them into `portal-files/first-boot/` on the file server. diff --git a/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/lightdm/RPiSystem_dark.png b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/lightdm/RPiSystem_dark.png new file mode 100644 index 0000000..2e3361e Binary files /dev/null and b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/lightdm/RPiSystem_dark.png differ diff --git a/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.plymouth b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.plymouth new file mode 100644 index 0000000..2d7a1f7 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.plymouth @@ -0,0 +1,8 @@ +[Plymouth Theme] +Name=Custom Splash +Description=Custom boot splash screen +ModuleName=script + +[script] +ImageDir=/usr/share/plymouth/themes/custom +ScriptFile=/usr/share/plymouth/themes/custom/custom.script diff --git a/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.script b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.script new file mode 100644 index 0000000..7de61c8 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/custom.script @@ -0,0 +1,40 @@ +screen_width = Window.GetWidth(); +screen_height = Window.GetHeight(); + +theme_image = Image("splash.png"); +image_width = theme_image.GetWidth(); +image_height = theme_image.GetHeight(); + +scale_x = image_width / screen_width; +scale_y = image_height / screen_height; + +if (scale_x > 1 || scale_y > 1) +{ + if (scale_x > scale_y) + { + resized_image = theme_image.Scale(screen_width, image_height / scale_x); + image_x = 0; + image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2; + } + else + { + resized_image = theme_image.Scale(image_width / scale_y, screen_height); + image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2; + image_y = 0; + } +} +else +{ + resized_image = theme_image.Scale(image_width, image_height); + image_x = (screen_width - image_width) / 2; + image_y = (screen_height - image_height) / 2; +} + +if (Plymouth.GetMode() != "shutdown") +{ + sprite = Sprite(resized_image); + sprite.SetPosition(image_x, image_y, -100); +} + +fun message_callback(text) { +} diff --git a/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/splash.png b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/splash.png new file mode 100644 index 0000000..7eb3c01 Binary files /dev/null and b/chromium-setup/emmc-provisioning/cloud-init/files-from-guard/plymouth-custom/splash.png differ diff --git a/chromium-setup/emmc-provisioning/cloud-init/first-boot.md b/chromium-setup/emmc-provisioning/cloud-init/first-boot.md index 3e38942..2196975 100644 --- a/chromium-setup/emmc-provisioning/cloud-init/first-boot.md +++ b/chromium-setup/emmc-provisioning/cloud-init/first-boot.md @@ -4,6 +4,22 @@ This script runs once on first boot via cloud-init (see `user-data-remote-gnss.e --- +## Structure (sections) + +1. **Constants** — `FILE_SERVER`, `PI_USER`, paths, log file. +2. **Logging** — All output tee’d to `/var/log/first-boot.log`. +3. **Helpers** — `install_oneshot(name)` downloads `${name}.sh` from the file server and installs it as a one-shot autostart (runs once at pi’s first login, then deletes itself). +4. **Packages** — git, Chromium, wmctrl, SSH, KDE Plasma, kscreen, maliit, xinput-calibrator. +5. **Kiosk files** — Download `start-chromium.sh` and `chromium-kiosk.desktop`; create autostart dir. +6. **Boot splash and wallpaper** — Download `splash.png`; install Plymouth custom theme; copy image for LightDM and desktop. +7. **LightDM** — Download `99-default-session.conf` and `99-wallpaper.conf` to `/etc/lightdm/lightdm.conf.d/`. +8. **KDE + Maliit** — Download `kdeglobals`, `kwinrc`, `maliit-keyboard.desktop` from file server to pi’s `.config` and autostart. +9. **reTerminal DM drivers** — Seeed repo clone and `reTerminal.sh`. +10. **One-shots** — Download `set-rotation-once.sh` + `.desktop` and `set-wallpaper-once.sh` + `.desktop` from file server. +11. **Reboot.** + +--- + ## Script header and environment - **`set -e`** — Exit immediately if any command fails. @@ -11,6 +27,16 @@ This script runs once on first boot via cloud-init (see `user-data-remote-gnss.e --- +## Logging + +All script output (stdout and stderr) is appended to **`/var/log/first-boot.log`** so you can review what ran after first boot (e.g. over SSH: `cat /var/log/first-boot.log`). + +- **`LOGFILE`** — Path of the log file (`/var/log/first-boot.log`). +- **`log "..."`** — Prints a timestamped line (ISO format); used for section headers. +- **`exec > >(tee -a "$LOGFILE") 2>&1`** — Sends all subsequent stdout and stderr to both the console and the log file. + +--- + ## Packages Installs the software needed for the rest of the script and for the kiosk: @@ -22,6 +48,7 @@ Installs the software needed for the rest of the script and for the kiosk: | **wmctrl** | Window control; used to force Chromium into fullscreen. | | **openssh-server** | SSH access (often also enabled in user-data). | | **kde-plasma-desktop** | KDE Plasma desktop (X11 session used for Chromium). | +| **kscreen** | KDE display control; provides `kscreen-doctor` for screen rotation (same as GUI “Right”). | | **maliit-keyboard** | On-screen keyboard for touch input. | | **xinput-calibrator** | Touchscreen calibration (optional; run manually if needed). | @@ -35,9 +62,9 @@ Creates `/home/pi/.config/autostart` so that `.desktop` files placed there are s ## Chromium kiosk files (from file server) -Does **not** create the kiosk files locally; it downloads them from your file server: +Downloads from `FILE_SERVER` (no local creation): -- **`FILE_SERVER`** — Base URL (default: `http://10.130.60.141:5000/files`). Change this if your server is elsewhere. +- **`FILE_SERVER`** — Base URL for first-boot assets (default: `http://10.130.60.141:5000/files/first-boot`). All first-boot files are served from a **first-boot** subfolder on the file server. Change this if your server or path is different. - **`start-chromium.sh`** — Downloaded to `/home/pi/start-chromium.sh`, made executable (755), owned by `pi`. This script waits for the desktop, starts Chromium in kiosk mode (e.g. `--app=...`), and uses `wmctrl` to force fullscreen. - **`chromium-kiosk.desktop`** — Downloaded to `/home/pi/.config/autostart/chromium-kiosk.desktop`, mode 644, owned by `pi`. This autostart entry runs `start-chromium.sh` when `pi` logs in. @@ -45,6 +72,16 @@ Ensure the `.desktop` file on the server has `Exec=/home/pi/start-chromium.sh` ( --- +## Boot splash and wallpaper (single image from file server) + +A **single image** (`splash.png`) is used for the boot splash, login screen, and desktop wallpaper. Host it at **`${FILE_SERVER}/splash.png`** (e.g. `http://10.130.60.141:5000/files/splash.png`). + +- **Plymouth (boot splash):** Downloads `splash.png`, `custom.plymouth`, and `custom.script` from the file server → installs to `/usr/share/plymouth/themes/custom/` → sets `Theme=custom` in `/etc/plymouth/plymouthd.conf` → runs `update-initramfs -u`. If any download fails, logs a warning and continues. +- **LightDM (login screen):** Copies the same image to `/usr/share/rpd-wallpaper/splash.png` and writes `/etc/lightdm/lightdm.conf.d/99-wallpaper.conf` with `wallpaper=...` and `wallpaper_mode=crop`. +- **Desktop wallpaper:** A one-shot autostart runs when **pi** first logs in and runs `plasma-apply-wallpaperimage /usr/share/rpd-wallpaper/splash.png`. The script then deletes itself. If `plasma-apply-wallpaperimage` is not installed, the step no-ops and the one-shot still removes itself. + +--- + ## KDE Plasma: default session (X11) Writes `/etc/lightdm/lightdm.conf.d/99-default-session.conf` so the display manager (LightDM) uses the **Plasma X11** session (`plasmax11`) instead of Wayland. Chromium kiosk is configured for X11, so this is required for it to run correctly. @@ -92,14 +129,26 @@ These changes take effect after a reboot. --- +## Screen rotation (portrait → landscape, “Right”) + +The reTerminal DM default is portrait. Rotation is set the **same way as in the GUI** (Display settings → Orientation → Right), using KDE’s **kscreen-doctor**, so nothing is written to `/boot/firmware/config.txt`. + +- **One-shot autostart** — A small script runs once when user `pi` first logs into the graphical session: + - **`/home/pi/set-rotation-once.sh`** — Waits 5 seconds, then runs `kscreen-doctor output.DSI-1.rotation.right` (only the reTerminal DM DSI-1 display; no other screen is used). + - **`/home/pi/.config/autostart/set-rotation-once.desktop`** — Autostart entry that runs the script. Both the script and this desktop file **delete themselves** after a successful run, so rotation is applied only once. +- **Effect** — Same as choosing “Right” in System Settings → Display → Orientation. Rotation applies after the first login; no reboot needed for rotation (only for the Seeed drivers). + +--- + ## Reboot -Runs **`reboot`** so the kernel and display stack load the new Seeed drivers. After reboot, the screen and touch should work, and the next login as `pi` will start the Chromium kiosk and Maliit via the autostart entries. +Runs **`reboot`** so the kernel and display stack load the new Seeed drivers. After reboot, the screen and touch work; on **pi**’s first login the one-shot sets rotation to “Right” (landscape), and the Chromium kiosk and Maliit start via autostart. --- ## Customisation -- **File server** — Edit `FILE_SERVER` if your `start-chromium.sh` and `chromium-kiosk.desktop` are served from another host/port. +- **File server** — Edit `FILE_SERVER` if your assets are served from another host/port. Host all files listed in **`files-from-guard/README.md`** and **`config-files/README.md`** (kiosk, splash, Plymouth, LightDM, KDE, Maliit, one-shots and their .desktop files). - **Kiosk URL** — The URL Chromium opens is defined in `start-chromium.sh` on your file server (e.g. `--app=http://127.0.0.1:8080`); change it there. - **User** — If you use a user other than `pi`, replace `pi` in this script and in the files on the file server (paths and ownership). +- **Screen rotation** — The one-shot runs `kscreen-doctor output.DSI-1.rotation.right`. To use another orientation, edit the script and change `rotation.right` to `rotation.left`, `rotation.inverted`, or `rotation.none`. diff --git a/chromium-setup/emmc-provisioning/cloud-init/first-boot.sh b/chromium-setup/emmc-provisioning/cloud-init/first-boot.sh index bd1a175..53a54e6 100644 --- a/chromium-setup/emmc-provisioning/cloud-init/first-boot.sh +++ b/chromium-setup/emmc-provisioning/cloud-init/first-boot.sh @@ -1,81 +1,96 @@ #!/bin/bash -# First-boot script: install packages, Chromium kiosk, KDE Plasma + touch. -# Intended to be downloaded and run by cloud-init (see user-data-remote-gnss.example). -# Run as root. +# First-boot: packages, Chromium kiosk, KDE Plasma + touch, reTerminal DM drivers. +# Run by cloud-init (user-data-remote-gnss.example). Run as root. set -e - export DEBIAN_FRONTEND=noninteractive -# Packages +# --- Constants --- +# All first-boot assets live in portal-files/first-boot/ on the file server. +FILE_SERVER="http://10.130.60.141:5000/files/first-boot" +PI_USER="pi" +PI_HOME="/home/$PI_USER" +AUTOSTART="$PI_HOME/.config/autostart" +LOGFILE="/var/log/first-boot.log" +PLYMOUTH_DIR="/usr/share/plymouth/themes/custom" +WALLPAPER_PATH="/usr/share/rpd-wallpaper/splash.png" + +# --- Logging --- +log() { echo "[$(date -Iseconds)] $*"; } +exec > >(tee -a "$LOGFILE") 2>&1 +log "=== first-boot.sh started ===" + +# --- Helpers --- +# Download script + .desktop from FILE_SERVER and install as one-shot autostart (runs once at pi's first login, then deletes itself). +install_oneshot() { + local name="$1" + log "--- Installing one-shot: $name ---" + curl -fsSL "${FILE_SERVER}/${name}.sh" -o "$PI_HOME/${name}.sh" || { log "WARNING: Could not download ${name}.sh"; return 1; } + curl -fsSL "${FILE_SERVER}/${name}.desktop" -o "$AUTOSTART/${name}.desktop" || { log "WARNING: Could not download ${name}.desktop"; return 1; } + chmod 755 "$PI_HOME/${name}.sh" && chmod 644 "$AUTOSTART/${name}.desktop" + chown "$PI_USER:$PI_USER" "$PI_HOME/${name}.sh" "$AUTOSTART/${name}.desktop" +} + +# --- 1. Packages --- +log "--- Installing packages ---" apt-get update -qq -apt-get install -y -qq \ - git \ - chromium-browser \ - wmctrl \ - openssh-server \ - kde-plasma-desktop \ - maliit-keyboard \ - xinput-calibrator +apt-get install -y -qq git chromium-browser wmctrl openssh-server \ + kde-plasma-desktop kscreen maliit-keyboard xinput-calibrator -# Autostart dir for user pi -mkdir -p /home/pi/.config/autostart +# --- 2. Dirs and kiosk files from file server --- +log "--- Kiosk files ---" +mkdir -p "$AUTOSTART" +curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o "$PI_HOME/start-chromium.sh" +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" -# Chromium kiosk files from file server -FILE_SERVER="http://10.130.60.141:5000/files" -curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o /home/pi/start-chromium.sh -chmod 755 /home/pi/start-chromium.sh -chown pi:pi /home/pi/start-chromium.sh -curl -fsSL "${FILE_SERVER}/chromium-kiosk.desktop" -o /home/pi/.config/autostart/chromium-kiosk.desktop -chmod 644 /home/pi/.config/autostart/chromium-kiosk.desktop -chown pi:pi /home/pi/.config/autostart/chromium-kiosk.desktop +# --- 3. Boot splash and wallpaper (splash.png + Plymouth theme from file server) --- +log "--- Boot splash and wallpaper ---" +mkdir -p "$PLYMOUTH_DIR" /usr/share/rpd-wallpaper +if curl -fsSL "${FILE_SERVER}/splash.png" -o "$PLYMOUTH_DIR/splash.png"; then + 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" + else + log "WARNING: Could not download custom.plymouth/custom.script; boot splash theme may be incomplete" + fi + grep -q '^Theme=custom' /etc/plymouth/plymouthd.conf 2>/dev/null || printf '%s\n' '[Daemon]' 'Theme=custom' >> /etc/plymouth/plymouthd.conf + 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" + log "Splash and wallpaper set from file server" +else + log "WARNING: Could not download splash.png" +fi -# KDE Plasma: default session (X11 for Chromium) +# --- 4. LightDM: KDE Plasma X11 session + configs from file server --- +log "--- LightDM session ---" mkdir -p /etc/lightdm/lightdm.conf.d -cat > /etc/lightdm/lightdm.conf.d/99-default-session.conf << 'LIGHTDM' -[Seat:*] -user-session=plasmax11 -LIGHTDM +curl -fsSL "${FILE_SERVER}/99-default-session.conf" -o /etc/lightdm/lightdm.conf.d/99-default-session.conf 2>/dev/null || log "WARNING: Could not download 99-default-session.conf" -# KDE touch-friendly -cat > /home/pi/.config/kdeglobals << 'KDE' -[General] -ForceFontDPI=120 -KDE -chown pi:pi /home/pi/.config/kdeglobals -chmod 644 /home/pi/.config/kdeglobals - -cat > /home/pi/.config/kwinrc << 'KWIN' -[Windows] -BorderlessMaximizedWindows=true -[Plugins] -touchpointsEnabled=true -KWIN -chown pi:pi /home/pi/.config/kwinrc -chmod 644 /home/pi/.config/kwinrc - -# On-screen keyboard (maliit) -cat > /home/pi/.config/autostart/maliit-keyboard.desktop << 'MALIIT' -[Desktop Entry] -Type=Application -Name=Maliit Keyboard -Exec=maliit-keyboard -r -X-GNOME-Autostart-enabled=true -MALIIT -chown pi:pi /home/pi/.config/autostart/maliit-keyboard.desktop -chmod 644 /home/pi/.config/autostart/maliit-keyboard.desktop - -# Ownership for all of pi's config -chown -R pi:pi /home/pi/.config - -# Set KDE Plasma (X11) as default session +# --- 5. KDE touch-friendly + Maliit (from file server) --- +log "--- KDE and Maliit ---" +mkdir -p "$AUTOSTART" "$PI_HOME/.config" +curl -fsSL "${FILE_SERVER}/kdeglobals" -o "$PI_HOME/.config/kdeglobals" 2>/dev/null || log "WARNING: Could not download kdeglobals" +curl -fsSL "${FILE_SERVER}/kwinrc" -o "$PI_HOME/.config/kwinrc" 2>/dev/null || log "WARNING: Could not download kwinrc" +curl -fsSL "${FILE_SERVER}/maliit-keyboard.desktop" -o "$AUTOSTART/maliit-keyboard.desktop" 2>/dev/null || log "WARNING: Could not download maliit-keyboard.desktop" +chown -R "$PI_USER:$PI_USER" "$PI_HOME/.config" update-alternatives --set x-session-manager /usr/bin/startplasma-x11 2>/dev/null || true -# reTerminal DM: install Seeed display/touch drivers (screen will work after reboot) +# --- 6. reTerminal DM drivers (Seeed) --- +log "--- reTerminal DM drivers ---" REPO_DIR="/tmp/seeed-linux-dtoverlays" git clone --depth 1 https://github.com/Seeed-Studio/seeed-linux-dtoverlays "$REPO_DIR" "$REPO_DIR/scripts/reTerminal.sh" --device reTerminal-DM rm -rf "$REPO_DIR" -# Reboot so display and touch work +# --- 7. One-shots (rotation + wallpaper at first login) --- +install_oneshot set-rotation-once || true +install_oneshot set-wallpaper-once || true + +# --- 8. Reboot --- +log "=== first-boot.sh finished, rebooting ===" reboot diff --git a/chromium-setup/emmc-provisioning/cloud-init/plymouth-custom.script b/chromium-setup/emmc-provisioning/cloud-init/plymouth-custom.script new file mode 100644 index 0000000..7de61c8 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/plymouth-custom.script @@ -0,0 +1,40 @@ +screen_width = Window.GetWidth(); +screen_height = Window.GetHeight(); + +theme_image = Image("splash.png"); +image_width = theme_image.GetWidth(); +image_height = theme_image.GetHeight(); + +scale_x = image_width / screen_width; +scale_y = image_height / screen_height; + +if (scale_x > 1 || scale_y > 1) +{ + if (scale_x > scale_y) + { + resized_image = theme_image.Scale(screen_width, image_height / scale_x); + image_x = 0; + image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2; + } + else + { + resized_image = theme_image.Scale(image_width / scale_y, screen_height); + image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2; + image_y = 0; + } +} +else +{ + resized_image = theme_image.Scale(image_width, image_height); + image_x = (screen_width - image_width) / 2; + image_y = (screen_height - image_height) / 2; +} + +if (Plymouth.GetMode() != "shutdown") +{ + sprite = Sprite(resized_image); + sprite.SetPosition(image_x, image_y, -100); +} + +fun message_callback(text) { +} diff --git a/chromium-setup/emmc-provisioning/cloud-init/set-rotation-once.sh b/chromium-setup/emmc-provisioning/cloud-init/set-rotation-once.sh new file mode 100644 index 0000000..250c2c3 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/set-rotation-once.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# One-shot: set DSI-1 (reTerminal DM) rotation to Right, then remove self. Runs as user pi at first login. +export DISPLAY=:0 +sleep 5 +kscreen-doctor output.DSI-1.rotation.right +rm -f /home/pi/.config/autostart/set-rotation-once.desktop /home/pi/set-rotation-once.sh diff --git a/chromium-setup/emmc-provisioning/cloud-init/set-wallpaper-once.sh b/chromium-setup/emmc-provisioning/cloud-init/set-wallpaper-once.sh new file mode 100644 index 0000000..6c14e89 --- /dev/null +++ b/chromium-setup/emmc-provisioning/cloud-init/set-wallpaper-once.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# One-shot: set desktop wallpaper to splash image, then remove self. Runs as user pi at first login. +export DISPLAY=:0 +sleep 8 +WALLPAPER="/usr/share/rpd-wallpaper/splash.png" +[[ -f "$WALLPAPER" ]] && plasma-apply-wallpaperimage "$WALLPAPER" 2>/dev/null || true +rm -f /home/pi/.config/autostart/set-wallpaper-once.desktop /home/pi/set-wallpaper-once.sh diff --git a/chromium-setup/emmc-provisioning/dashboard/app.py b/chromium-setup/emmc-provisioning/dashboard/app.py index bc3b5f5..7344844 100644 --- a/chromium-setup/emmc-provisioning/dashboard/app.py +++ b/chromium-setup/emmc-provisioning/dashboard/app.py @@ -38,6 +38,7 @@ BUILD_REQUEST_FILE = Path(os.environ.get("CM4_BUILD_REQUEST_FILE", str(BASE_DIR 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"))) 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"))) @@ -395,15 +396,32 @@ def admin(): return render_template("admin.html", username=session.get("username", "Admin")) +@app.route("/admin/portal-files") +@require_admin +def admin_portal_files(): + return render_template("portal_files.html", username=session.get("username", "Admin")) + + +@app.route("/admin/cloudinit-build") +@require_admin +def admin_cloudinit_build(): + return render_template("cloudinit_build.html", username=session.get("username", "Admin")) + + # Serve portal files for wget (e.g. cloud-init first boot). No auth. +# Subpaths allowed (e.g. first-boot/splash.png); ".." and "\\" forbidden to prevent traversal. @app.route("/files/") def serve_portal_file(filename): - if ".." in filename or "/" in filename or "\\" in filename: + if ".." in filename or "\\" in filename: + return jsonify({"error": "invalid path"}), 400 + path = (PORTAL_FILES_DIR / filename).resolve() + try: + path.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: return jsonify({"error": "invalid path"}), 400 - path = PORTAL_FILES_DIR / filename if not path.is_file(): return jsonify({"error": "not found"}), 404 - return send_file(path, as_attachment=False, download_name=filename) + return send_file(path, as_attachment=False, download_name=filename.split("/")[-1]) @app.route("/api/status") @@ -571,20 +589,95 @@ def api_cloudinit_images(): return jsonify({"images": list_cloudinit_images(), "cloudinit_dir": str(CLOUDINIT_IMAGES_DIR)}) +def _load_portal_descriptions(): + """Return dict mapping path -> description (path can be file or folder).""" + if not PORTAL_DESCRIPTIONS_FILE.is_file(): + return {} + try: + with open(PORTAL_DESCRIPTIONS_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + return {} + + +def _save_portal_descriptions(descriptions): + try: + PORTAL_DESCRIPTIONS_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(PORTAL_DESCRIPTIONS_FILE, "w", encoding="utf-8") as f: + json.dump(descriptions, f, indent=2) + return True + except OSError: + return False + + @app.route("/api/portal-files") @require_admin def api_portal_files_list(): + """List one level: root or contents of path=... (folders and files).""" + subpath = request.args.get("path", "").strip().strip("/") + if ".." in subpath or "\\" in subpath: + return jsonify({"items": [], "base_url": request.host_url.rstrip("/") + "/files/", "descriptions": {}, "current_path": ""}) if not PORTAL_FILES_DIR.is_dir(): - return jsonify({"files": [], "base_url": request.host_url.rstrip("/") + "/files/"}) - files = [] - for p in sorted(PORTAL_FILES_DIR.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True): - if p.is_file() and ".." not in p.name: - try: - files.append({"name": p.name, "size": p.stat().st_size, "mtime": p.stat().st_mtime}) - except OSError: - pass + return jsonify({"items": [], "base_url": request.host_url.rstrip("/") + "/files/", "descriptions": {}, "current_path": ""}) + list_dir = (PORTAL_FILES_DIR / subpath).resolve() if subpath else PORTAL_FILES_DIR + try: + list_dir.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"items": [], "base_url": request.host_url.rstrip("/") + "/files/", "descriptions": {}, "current_path": subpath}) + if not list_dir.is_dir(): + return jsonify({"items": [], "base_url": request.host_url.rstrip("/") + "/files/", "descriptions": {}, "current_path": subpath}) + items = [] + for p in sorted(list_dir.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())): + if ".." in p.name or p.name.startswith("."): + continue + rel = (subpath + "/" + p.name) if subpath else p.name + try: + if p.is_dir(): + items.append({"type": "folder", "path": rel, "name": p.name}) + else: + items.append({"type": "file", "path": rel, "name": p.name, "size": p.stat().st_size, "mtime": p.stat().st_mtime}) + except OSError: + pass + descriptions = _load_portal_descriptions() base = request.host_url.rstrip("/") + "/files/" - return jsonify({"files": files, "base_url": base}) + return jsonify({"items": items, "base_url": base, "descriptions": descriptions, "current_path": subpath}) + + +@app.route("/api/portal-files/descriptions", methods=["GET", "PATCH"]) +@require_admin +def api_portal_descriptions(): + if request.method == "PATCH": + data = request.get_json(force=True, silent=True) or {} + desc = data.get("descriptions") + if not isinstance(desc, dict): + return jsonify({"ok": False, "error": "descriptions must be a dict"}), 400 + if not _save_portal_descriptions(desc): + return jsonify({"ok": False, "error": "save failed"}), 500 + admin_log("portal_descriptions", "updated") + return jsonify({"ok": True}) + return jsonify({"descriptions": _load_portal_descriptions()}) + + +@app.route("/api/portal-files/folder", methods=["POST"]) +@require_admin +def api_portal_folder_create(): + data = request.get_json(force=True, silent=True) or {} + path = (data.get("path") or "").strip().strip("/") + if not path: + return jsonify({"ok": False, "error": "path required"}), 400 + if ".." in path or "\\" in path: + return jsonify({"ok": False, "error": "invalid path"}), 400 + full = (PORTAL_FILES_DIR / path).resolve() + try: + full.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"ok": False, "error": "invalid path"}), 400 + try: + full.mkdir(parents=True, exist_ok=True) + admin_log("portal_folder_create", path) + return jsonify({"ok": True, "path": path}) + except OSError as e: + return jsonify({"ok": False, "error": str(e)}), 500 @app.route("/api/portal-files/upload", methods=["POST"]) @@ -595,12 +688,20 @@ def api_portal_files_upload(): f = request.files.get("file") or request.files.get("upload") if not f or not f.filename: return jsonify({"ok": False, "error": "no file selected"}), 400 - name = re.sub(r"[^\w\-.]", "_", f.filename)[:120] or "upload" - if ".." in name or name.startswith("/"): + base_name = re.sub(r"[^\w\-./]", "_", f.filename)[:120] or "upload" + if ".." in base_name or base_name.startswith("/"): return jsonify({"ok": False, "error": "invalid filename"}), 400 - path = PORTAL_FILES_DIR / name + subpath = (request.form.get("path") or "").strip().strip("/") + if subpath and (".." in subpath or "\\" in subpath): + return jsonify({"ok": False, "error": "invalid path"}), 400 + name = (subpath + "/" + base_name) if subpath else base_name + path = (PORTAL_FILES_DIR / name).resolve() try: - PORTAL_FILES_DIR.mkdir(parents=True, exist_ok=True) + path.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"ok": False, "error": "invalid path"}), 400 + try: + path.parent.mkdir(parents=True, exist_ok=True) f.save(str(path)) admin_log("portal_upload", name) return jsonify({"ok": True, "name": name, "url": request.host_url.rstrip("/") + "/files/" + name}) @@ -613,17 +714,30 @@ def api_portal_files_upload(): @app.route("/api/portal-files/", methods=["DELETE"]) @require_admin def api_portal_file_delete(name): - if ".." in name or "/" in name or "\\" in name: + if ".." in name or "\\" in name: return jsonify({"ok": False, "error": "invalid name"}), 400 - path = PORTAL_FILES_DIR / name - if not path.is_file(): - return jsonify({"ok": False, "error": "not found"}), 404 + path = (PORTAL_FILES_DIR / name).resolve() try: - path.unlink() - admin_log("portal_delete", name) - return jsonify({"ok": True}) - except OSError as e: - return jsonify({"ok": False, "error": str(e)}), 500 + path.relative_to(PORTAL_FILES_DIR.resolve()) + except ValueError: + return jsonify({"ok": False, "error": "invalid path"}), 400 + if path.is_file(): + try: + path.unlink() + admin_log("portal_delete", name) + return jsonify({"ok": True}) + except OSError as e: + return jsonify({"ok": False, "error": str(e)}), 500 + if path.is_dir(): + if any(path.iterdir()): + return jsonify({"ok": False, "error": "folder not empty"}), 400 + try: + path.rmdir() + admin_log("portal_folder_delete", name) + return jsonify({"ok": True}) + except OSError as e: + return jsonify({"ok": False, "error": str(e)}), 500 + return jsonify({"ok": False, "error": "not found"}), 404 @app.route("/api/backups/upload", methods=["POST"]) diff --git a/chromium-setup/emmc-provisioning/dashboard/templates/admin.html b/chromium-setup/emmc-provisioning/dashboard/templates/admin.html index 1fe499a..d1a7260 100644 --- a/chromium-setup/emmc-provisioning/dashboard/templates/admin.html +++ b/chromium-setup/emmc-provisioning/dashboard/templates/admin.html @@ -21,6 +21,9 @@ .header span { color: var(--text-dim); font-size: 0.9rem; } .header a { color: var(--text-dim); text-decoration: none; margin-left: 0.5rem; } .header a:hover { color: var(--accent); } + .nav a { color: var(--text-dim); text-decoration: none; margin-right: 1rem; } + .nav a:hover { color: var(--accent); } + .nav a.active { color: var(--accent); font-weight: 600; } .section { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1rem; } .section-title { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-bottom: 0.75rem; display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; } .btn { padding: 0.4rem 0.8rem; font-size: 0.8rem; font-weight: 500; font-family: inherit; border-radius: var(--radius-sm); cursor: pointer; border: none; transition: background 0.15s, color 0.15s; } @@ -53,7 +56,14 @@
-

Admin

+
+

Admin

+ +
Logged in as {{ username }} Deploy Log out @@ -91,43 +101,6 @@
- -
-

Download & build cloud-init image

-

Download latest Raspberry Pi OS (arm64), inject cloud-init. Output goes to Cloud-init images above.

-
- - - -
-
-
-
-
- Edit cloud-init (user-data, meta-data, network-config) -
-

Templates: - -

-
    - - - - - - -
    -
    -
    - - -
    -

    Portal files (for wget in cloud-init)

    -

    Files here are served at /files/ — use in user-data e.g. curl -fsSL "http://THIS_SERVER/files/bootstrap.sh" -o /tmp/bootstrap.sh

    -
    FileSizeActions
    - -
    -

    Admin users

    @@ -227,26 +200,6 @@ authFetch('/api/cloudinit-images').then(function(r){ return r.json(); }).then(function(d){ renderCloudinit(d.images || []); }).catch(function(){}); } - function renderPortal(files, baseUrl) { - var tbody = document.getElementById('portalBody'); - var empty = document.getElementById('portalEmpty'); - document.getElementById('portalBaseUrl').textContent = baseUrl || ''; - tbody.innerHTML = ''; - if (!files || files.length === 0) { empty.style.display = 'block'; return; } - empty.style.display = 'none'; - files.forEach(function(f){ - var tr = document.createElement('tr'); - tr.innerHTML = ''+escapeHtml(f.name)+''+fmtSize(f.size)+''; - tbody.appendChild(tr); - }); - tbody.querySelectorAll('.delete-portal').forEach(function(btn){ - btn.onclick = function(){ var n = btn.getAttribute('data-name'); if (!confirm('Delete '+n+'?')) return; authFetch('/api/portal-files/'+encodeURIComponent(n), { method: 'DELETE' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchPortal(); else alert(d.error); }); }; - }); - } - function fetchPortal() { - authFetch('/api/portal-files').then(function(r){ return r.json(); }).then(function(d){ renderPortal(d.files || [], d.base_url); }).catch(function(){}); - } - function renderUsers(users) { var tbody = document.getElementById('usersBody'); tbody.innerHTML = ''; @@ -272,48 +225,6 @@ authFetch('/api/admin/logs').then(function(r){ return r.json(); }).then(function(d){ renderLogs(d.logs); }).catch(function(){}); } - function fetchBuildStatus() { - authFetch('/api/build-cloudinit-status').then(function(r){ return r.json(); }).then(function(d){ - var el = document.getElementById('buildCloudInitStatus'); - var btn = document.getElementById('buildCloudInitBtn'); - var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0; - if(btn) btn.disabled = busy; - if(d.phase === 'idle' && !d.message) el.textContent = ''; - else if(d.phase === 'done') { el.textContent = 'Done: '+ (d.output_name||'') + ' — see Cloud-init images above.'; fetchCloudinit(); fetchGolden(); } - else if(d.phase === 'error') el.textContent = 'Error: ' + (d.error||''); - else el.textContent = (d.phase||'') + ': ' + (d.message||''); - if(busy) setTimeout(fetchBuildStatus, 5000); - }).catch(function(){}); - } - function startBuild() { - var btn = document.getElementById('buildCloudInitBtn'); - if(btn) btn.disabled = true; - var body = { variant: document.getElementById('buildVariant').value, set_as_golden_after: document.getElementById('buildSetGolden').checked }; - var ud = document.getElementById('buildUserData').value.trim(); - var md = document.getElementById('buildMetaData').value.trim(); - var nc = document.getElementById('buildNetworkConfig').value.trim(); - if(ud) body.user_data = ud; if(md) body.meta_data = md; if(nc) body.network_config = nc; - authFetch('/api/build-cloudinit', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r){ return r.json(); }).then(function(d){ - if(d.ok) { document.getElementById('buildCloudInitStatus').textContent = 'Build started…'; setTimeout(fetchBuildStatus, 2000); } - else { alert(d.error); if(btn) btn.disabled = false; } - }).catch(function(){ if(btn) btn.disabled = false; }); - } - - function fetchTemplates() { - authFetch('/api/cloudinit-templates').then(function(r){ return r.json(); }).then(function(d){ - var list = d.templates || []; - var sel = document.getElementById('buildTemplateSelect'); - sel.innerHTML = ''; - list.forEach(function(t){ var o = document.createElement('option'); o.value = t.id; o.textContent = t.name; sel.appendChild(o); }); - var ul = document.getElementById('buildTemplateList'); - ul.innerHTML = list.map(function(t){ return '
  • '+escapeHtml(t.name)+'
  • '; }).join('') || '
  • No templates
  • '; - ul.querySelectorAll('.load-tpl').forEach(function(b){ b.onclick = function(){ authFetch('/api/cloudinit-templates/'+b.getAttribute('data-id')).then(function(r){ return r.json(); }).then(function(t){ document.getElementById('buildUserData').value = t.user_data||''; document.getElementById('buildMetaData').value = t.meta_data||''; document.getElementById('buildNetworkConfig').value = t.network_config||''; }); }; }); - ul.querySelectorAll('.del-tpl').forEach(function(b){ b.onclick = function(){ if(!confirm('Delete template?')) return; authFetch('/api/cloudinit-templates/'+b.getAttribute('data-id'), { method: 'DELETE' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchTemplates(); }); }; }); - }).catch(function(){}); - } - function loadTemplateFromSelect() { var s = document.getElementById('buildTemplateSelect'); if(s && s.value) authFetch('/api/cloudinit-templates/'+s.value).then(function(r){ return r.json(); }).then(function(t){ document.getElementById('buildUserData').value = t.user_data||''; document.getElementById('buildMetaData').value = t.meta_data||''; document.getElementById('buildNetworkConfig').value = t.network_config||''; }); } - function saveTemplate() { var name = prompt('Template name'); if(!name || !name.trim()) return; var body = { name: name.trim(), user_data: document.getElementById('buildUserData').value, meta_data: document.getElementById('buildMetaData').value, network_config: document.getElementById('buildNetworkConfig').value }; authFetch('/api/cloudinit-templates', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { fetchTemplates(); alert('Saved'); } else alert(d.error); }); } - document.getElementById('refreshBackupsBtn').onclick = fetchBackups; document.getElementById('uploadBackupBtn').onclick = function(){ document.getElementById('uploadBackupInput').click(); }; document.getElementById('uploadBackupInput').onchange = function(){ @@ -324,18 +235,6 @@ this.value = ''; }; document.getElementById('refreshCloudinitBtn').onclick = fetchCloudinit; - document.getElementById('buildCloudInitBtn').onclick = startBuild; - document.getElementById('buildVariant').onchange = function(){ authFetch('/api/raspios-latest-url?variant='+encodeURIComponent(document.getElementById('buildVariant').value)).then(function(r){ return r.json(); }).then(function(d){ document.getElementById('buildRaspiosUrl').textContent = d.ok && d.filename ? d.filename : (d.error||''); }); }; - document.getElementById('buildTemplateLoad').onclick = loadTemplateFromSelect; - document.getElementById('buildTemplateSave').onclick = saveTemplate; - document.getElementById('uploadPortalBtn').onclick = function(){ document.getElementById('uploadPortalInput').click(); }; - document.getElementById('uploadPortalInput').onchange = function(){ - var f = this.files && this.files[0]; - if(!f) return; - var fd = new FormData(); fd.append('file', f); - authFetch('/api/portal-files/upload', { method: 'POST', body: fd }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { fetchPortal(); alert('Uploaded. URL: '+d.url); } else alert(d.error); }).catch(function(){}); - this.value = ''; - }; document.getElementById('refreshLogsBtn').onclick = fetchLogs; document.getElementById('addUserBtn').onclick = function(){ document.getElementById('addUserForm').style.display = 'block'; }; document.getElementById('addUserCancel').onclick = function(){ document.getElementById('addUserForm').style.display = 'none'; }; @@ -347,9 +246,7 @@ authFetch('/api/admin/users', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username: username, password: password}) }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) { document.getElementById('addUserForm').style.display = 'none'; document.getElementById('newUsername').value = ''; document.getElementById('newPassword').value = ''; fetchUsers(); } else alert(d.error); }); }; - fetchBackups(); fetchCloudinit(); fetchPortal(); fetchUsers(); fetchLogs(); fetchGolden(); fetchBuildStatus(); fetchTemplates(); - authFetch('/api/raspios-latest-url').then(function(r){ return r.json(); }).then(function(d){ document.getElementById('buildRaspiosUrl').textContent = d.ok && d.filename ? d.filename : (d.error||''); }); - setInterval(fetchBuildStatus, 15000); + fetchBackups(); fetchCloudinit(); fetchUsers(); fetchLogs(); fetchGolden(); setInterval(fetchLogs, 30000); diff --git a/chromium-setup/emmc-provisioning/dashboard/templates/cloudinit_build.html b/chromium-setup/emmc-provisioning/dashboard/templates/cloudinit_build.html new file mode 100644 index 0000000..dd44c35 --- /dev/null +++ b/chromium-setup/emmc-provisioning/dashboard/templates/cloudinit_build.html @@ -0,0 +1,213 @@ + + + + + + Cloud-init build · Admin · CM4 Provisioning + + + + + + +
    +
    +
    +

    Cloud-init build

    + +
    + Logged in as {{ username }} + Deploy + Log out +
    + +
    +

    Download & build cloud-init image

    +

    Download latest Raspberry Pi OS (arm64), inject cloud-init. Output goes to Cloud-init images on the Admin page.

    +
    + + + +
    +
    +
    +
    +
    + +
    +

    Edit cloud-init files (templates)

    +

    Choose a file to edit below. These contents are injected into the image when you run the build.

    +
    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + Templates: + + + +
    +
      +
      +
      + + + + diff --git a/chromium-setup/emmc-provisioning/dashboard/templates/portal_files.html b/chromium-setup/emmc-provisioning/dashboard/templates/portal_files.html new file mode 100644 index 0000000..7bd8d42 --- /dev/null +++ b/chromium-setup/emmc-provisioning/dashboard/templates/portal_files.html @@ -0,0 +1,197 @@ + + + + + + Portal files · Admin · CM4 Provisioning + + + + + + +
      +
      +
      +

      Portal files

      + +
      + Logged in as {{ username }} + Deploy + Log out +
      + +
      +
      + Portal files + + + +
      +

      Served at /files/ — use in cloud-init e.g. curl -fsSL "http://SERVER/files/first-boot/splash.png" -o /tmp/splash.png

      + + + +
      NameTypeSizeDescriptionActions
      + +
      +
      + + + +