Refactor first-boot process to introduce ordered execution and new one-shot scripts

Revise the first-boot script to implement a structured approach with 13 numbered steps, allowing for better control over the execution order. Introduce two new one-shot scripts: `01-set-rotation-once.sh` and `02-set-wallpaper-once.sh`, replacing the previous single script approach. Update documentation to reflect these changes, including the new configuration options for enabling/disabling steps and the revised file structure for one-shot scripts. Enhance the dashboard to display first-boot progress, improving user feedback during the initial setup.
This commit is contained in:
nearxos
2026-02-22 16:22:44 +02:00
parent 79a7f76a12
commit 16c796b8af
15 changed files with 431 additions and 188 deletions

View File

@@ -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})