From 8eab802affb3195f031d9995c813581202c7cba2 Mon Sep 17 00:00:00 2001 From: nestire Date: Wed, 16 Jul 2025 13:12:51 +0200 Subject: [PATCH 1/3] add usb Adapter chipset 174c:55aa ASMedia --- daemon/templates/cmdline.txt | 2 +- image/image-config/stage-nextbox/00-init/01-run.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/templates/cmdline.txt b/daemon/templates/cmdline.txt index 0a6ebf93..ba0048b6 100644 --- a/daemon/templates/cmdline.txt +++ b/daemon/templates/cmdline.txt @@ -1 +1 @@ -console=serial0,115200 console=tty1 root=PARTUUID=%%partuuid%% rootfstype=ext4 fsck.repair=yes usb-storage.quirks=0x152d:0x1561:u,0x152d:0xa578:u,0x152d:0x0576:u rootwait +console=serial0,115200 console=tty1 root=PARTUUID=%%partuuid%% rootfstype=ext4 fsck.repair=yes usb-storage.quirks=0x152d:0x1561:u,0x152d:0xa578:u,0x152d:0x0576:u,0x174c:55aa:u rootwait diff --git a/image/image-config/stage-nextbox/00-init/01-run.sh b/image/image-config/stage-nextbox/00-init/01-run.sh index db0c0ab0..68373178 100755 --- a/image/image-config/stage-nextbox/00-init/01-run.sh +++ b/image/image-config/stage-nextbox/00-init/01-run.sh @@ -6,11 +6,11 @@ #rm "${ROOTFS_DIR}/etc/localtime" # modify /boot/cmdline.txt (kernel-cmdline) to use quirks (no UAS, strict usb-storage) for SATA-USB adapter -sed -i -e 's/rootwait quiet/usb-storage.quirks=0x152d:0x1561:u,0x152d:0xa578:u,0x152d:0x0576:u rootwait quiet/g' "${ROOTFS_DIR}/boot/cmdline.txt" +sed -i -e 's/rootwait quiet/usb-storage.quirks=0x152d:0x1561:u,0x152d:0xa578:u,0x152d:0x0576:u,0x174c:55aa:u rootwait quiet/g' "${ROOTFS_DIR}/boot/cmdline.txt" # [x] 152d:1561 => 'Sabrent' adapter using 'JMicron Chipset' # [x] 152d:a578 => 'Sabrent' adapter using 'JMicron Chipset' used since 2025 # [x] 152d:0576 => 'Sabrent' adapter using 'JMicron Chipset' used since 2025 -# [ ] 174c:55aa => 'SKL' & 'Inateck' adapters using 'ASMedia Chipset' +# [x] 174c:55aa => 'SKL' & 'Inateck' adapters using 'ASMedia Chipset' # modify /boot/config.txt to activate i2c sed -i -e 's/#dtparam=i2c_arm=on/dtparam=i2c_arm=on/g' "${ROOTFS_DIR}/boot/config.txt" From e9bcc02a0720bf4c3abb6871792564d40d3d019f Mon Sep 17 00:00:00 2001 From: nestire Date: Wed, 16 Jul 2025 13:15:28 +0200 Subject: [PATCH 2/3] change comment --- image/image-config/stage-nextbox/00-init/01-run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/image/image-config/stage-nextbox/00-init/01-run.sh b/image/image-config/stage-nextbox/00-init/01-run.sh index 68373178..903e99af 100755 --- a/image/image-config/stage-nextbox/00-init/01-run.sh +++ b/image/image-config/stage-nextbox/00-init/01-run.sh @@ -10,7 +10,7 @@ sed -i -e 's/rootwait quiet/usb-storage.quirks=0x152d:0x1561:u,0x152d:0xa578:u,0 # [x] 152d:1561 => 'Sabrent' adapter using 'JMicron Chipset' # [x] 152d:a578 => 'Sabrent' adapter using 'JMicron Chipset' used since 2025 # [x] 152d:0576 => 'Sabrent' adapter using 'JMicron Chipset' used since 2025 -# [x] 174c:55aa => 'SKL' & 'Inateck' adapters using 'ASMedia Chipset' +# [x] 174c:55aa => 'Sabrent' adapter using 'ASMedia Chipset' used since 2025 # modify /boot/config.txt to activate i2c sed -i -e 's/#dtparam=i2c_arm=on/dtparam=i2c_arm=on/g' "${ROOTFS_DIR}/boot/config.txt" From 8a3d0d5a5f05b655d483114447b553ef85c076aa Mon Sep 17 00:00:00 2001 From: nestire Date: Wed, 23 Jul 2025 15:39:54 +0200 Subject: [PATCH 3/3] add janitor nextbox status page --- debian/Makefile | 17 +- debian/debian/install | 1 + debian/debian/rules | 1 + janitor/nextbox_status_page.py | 252 ++++++++++++++++++++++++++++ janitor/nextbox_status_page.service | 14 ++ 5 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 janitor/nextbox_status_page.py create mode 100644 janitor/nextbox_status_page.service diff --git a/debian/Makefile b/debian/Makefile index 22dd77b0..0049b6f2 100644 --- a/debian/Makefile +++ b/debian/Makefile @@ -21,6 +21,7 @@ unstable: -v $(shell pwd):/build \ -v $(shell pwd)/../app:/app \ -v $(shell pwd)/../daemon:/daemon \ + -v $(shell pwd)/../janitor:/janitor \ -p 8080:80 \ $(IMAGE_NAME):stable make PKG=nextbox-unstable GIT_TAG=main main-target @@ -33,6 +34,7 @@ testing: -v $(shell pwd):/build \ -v $(shell pwd)/../app:/app \ -v $(shell pwd)/../daemon:/daemon \ + -v $(shell pwd)/../janitor:/janitor \ -p 8080:80 \ $(IMAGE_NAME):stable make PKG=nextbox-testing GIT_TAG=main main-target @@ -45,6 +47,7 @@ stable: -v $(shell pwd):/build \ -v $(shell pwd)/../app:/app \ -v $(shell pwd)/../daemon:/daemon \ + -v $(shell pwd)/../janitor:/janitor \ -p 8080:80 \ $(IMAGE_NAME):stable make PKG=nextbox GIT_TAG=main main-target @@ -57,7 +60,7 @@ clean: make PKG=nextbox GIT_TAG=main deb-clean -main-target: $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/scripts $(PKG)/templates $(PKG)/nextbox-compose $(PKG)/debian $(PKG)/rtun-linux-arm64 deb-src +main-target: $(PKG)/janitor $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/scripts $(PKG)/templates $(PKG)/nextbox-compose $(PKG)/debian $(PKG)/rtun-linux-arm64 deb-src $(PKG)/debian: debian @@ -70,7 +73,7 @@ $(PKG)/debian: debian cp repos/daemon/services/nextbox.reverse-tunnel.service $(PKG)/debian/$(PKG).reverse-tunnel.service cp repos/daemon/services/nextbox.nextbox-factory-reset.service $(PKG)/debian/$(PKG).nextbox-factory-reset.service cp repos/daemon/services/nextbox.nextbox-updater.service $(PKG)/debian/$(PKG).nextbox-updater.service - + cp janitor/nextbox_status_page.service $(PKG)/debian/$(PKG).nextbox_status_page.service $(PKG)/app: repos/app/nextbox/js/nextbox-main.js mkdir -p $(PKG)/app/nextbox @@ -107,6 +110,11 @@ $(PKG)/templates: repos/daemon/templates $(PKG)/debian mkdir -p $(PKG)/templates cp repos/daemon/templates/* $(PKG)/templates + +$(PKG)/janitor: repos/janitor $(PKG)/debian + mkdir -p $(PKG)/janitor + cp repos/janitor/* $(PKG)/janitor + $(PKG)/rtun-linux-arm64: $(PKG)/debian cd $(PKG) && \ wget https://github.com/snsinfu/reverse-tunnel/releases/download/v1.3.0/rtun-linux-arm64 @@ -126,6 +134,7 @@ start-dev-docker: dev-image -v $(shell pwd):/build \ -v $(shell pwd)/../app:/app \ -v $(shell pwd)/../daemon:/daemon \ + -v $(shell pwd)/../janitor:/janitor \ -p 8080:80 \ $(IMAGE_NAME):stable @@ -140,7 +149,7 @@ dev-image: Dockerfile ### build source package ### -$(DEB_SRC): $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/nextbox-compose $(PKG)/debian +$(DEB_SRC): $(PKG)/janitor $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/nextbox-compose $(PKG)/debian cd $(PKG) && \ dpkg-buildpackage -S #debsign -k CBF5C9FD2105C32B1E9CDC2C0303797FE98B51CD nextbox_$(VERSION)_source.changes @@ -158,7 +167,7 @@ deb-clean: rm -f $(PKG)_$(VERSION).dsc rm -f $(PKG)_$(VERSION).tar.gz -deb-src: $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/nextbox-compose $(PKG)/debian +deb-src: $(PKG)/janitor $(PKG)/app $(PKG)/nextbox_daemon $(PKG)/nextbox-compose $(PKG)/debian make PKG=$(PKG) GIT_TAG=$(GIT_TAG) $(DEB_SRC) make PKG=$(PKG) GIT_TAG=$(GIT_TAG) upload diff --git a/debian/debian/install b/debian/debian/install index 009d5832..dac938b7 100644 --- a/debian/debian/install +++ b/debian/debian/install @@ -14,3 +14,4 @@ scripts/nextbox-desec-hook.sh /usr/bin scripts/nextbox-update-debian.sh /usr/bin scripts/nextbox-soft-reset.sh /usr/bin templates/* /usr/lib/nextbox-templates +janitor/nextbox_status_page.py /usr/bin/ diff --git a/debian/debian/rules b/debian/debian/rules index 78a6f790..f6eadb9a 100755 --- a/debian/debian/rules +++ b/debian/debian/rules @@ -15,3 +15,4 @@ override_dh_installsystemd: dh_installsystemd --name=reverse-tunnel --no-start --no-enable dh_installsystemd --name=nextbox-factory-reset --no-start --no-enable dh_installsystemd --name=nextbox-updater --no-start --no-enable --no-stop-on-upgrade + dh_installsystemd --name=nextbox_status_page diff --git a/janitor/nextbox_status_page.py b/janitor/nextbox_status_page.py new file mode 100644 index 00000000..0605bfcc --- /dev/null +++ b/janitor/nextbox_status_page.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +import subprocess +import socket +import datetime +import os +import html +import shutil +import signal +import sys +import re +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler + +# ------------------------------------------------------------------- +# CONFIGURATION +# ------------------------------------------------------------------- + +OUTPUT_HTML = "/var/www/html/status.html" +HTTP_PORT = 8080 +REFRESH_INTERVAL = 60 + +JOURNAL_PATTERNS = [ + ("UAS is ignored for this device, using usb-storage instead", True, "UAS not used warning, pressent"), + ("Can't start Nextcloud because upgrading", False, "No nextcloud upgrade block"), +] + +# Note: Added nextbox-daemon.server here +SERVICES = [ + "sshd.service", + "networking.service", + "docker.service", + "nextbox-daemon.service", +] + +# ------------------------------------------------------------------- +# HELPERS +# ------------------------------------------------------------------- + +def run_cmd(cmd): + p = subprocess.run(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, text=True) + return p.stdout.strip(), p.stderr.strip(), p.returncode + +def get_package_version(pkg_name): + """ + Returns the installed version of the given Debian package, + or an error string if the package is not installed. + """ + # dpkg-query exits non-zero if the package is not installed + out, err, rc = run_cmd(["dpkg-query", "-W", "-f=${Version}", pkg_name]) + if rc == 0 and out: + return out + elif rc == 0: + return "(no version string returned)" + else: + return f"(not installed or error: {err or out})" + +def get_host_info(): + """Gather hostname, uptime, and load averages (no disk anymore).""" + hostname = socket.gethostname() + + up_out, up_err, up_rc = run_cmd(["uptime", "-p"]) + uptime = up_out if up_rc == 0 else f"error: {up_err}" + + la_out, la_err, la_rc = run_cmd(["cat", "/proc/loadavg"]) + if la_rc == 0: + load1, load5, load15 = la_out.split()[:3] + else: + load1 = load5 = load15 = f"error: {la_err}" + + return { + "hostname": hostname, + "uptime": uptime, + "load1": load1, + "load5": load5, + "load15": load15, + } + +def check_journal_boot(patterns): + """ + Scan the entire journal of the current boot for each pattern. + Patterns should be an iterable of: + - (string, bool) => (pattern, should_exist) + - (string, bool, string) => (pattern, should_exist, label) + - bare string (=> must exist, label=pattern) + + Returns a dict mapping each label to (ok:bool, message:str). + """ + # Normalize into triplets (pattern, should_exist, label) + normalized = [] + for p in patterns: + if isinstance(p, tuple): + if len(p) == 2: + pattern, should_exist = p + label = pattern + elif len(p) == 3: + pattern, should_exist, label = p + else: + raise ValueError("Pattern tuples must be (pat, bool[, label])") + elif isinstance(p, str): + pattern, should_exist, label = p, True, p + else: + raise ValueError("Each pattern entry must be str or tuple") + + normalized.append((pattern, bool(should_exist), str(label))) + + # Grab whole journal for this boot + cmd = ["journalctl", "-b", "--no-pager", "--output=short-iso"] + out, err, rc = run_cmd(cmd) + if rc != 0: + # mark all as FAIL + return { + label: (False, f"journalctl exit code {rc}, err={err}") + for _, _, label in normalized + } + + text = out.lower() + results = {} + for pattern, should_exist, label in normalized: + found = (pattern.lower() in text) + if should_exist: + if found: + results[label] = (True, f"Found required pattern") + else: + results[label] = (False, f"Missing required pattern") + else: + if found: + results[label] = (False, f"Forbidden pattern was found") + else: + results[label] = (True, f"Forbidden pattern not present") + return results + +def check_services(services): + status = {} + for svc in services: + out, err, rc = run_cmd(["systemctl", "is-active", svc]) + if rc == 0: + status[svc] = out + else: + status[svc] = (out or err or "unknown").strip() + return status + +def render_html(host, nextbox_version, journal_checks, services): + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + parts = [ + "", + "", + f"Status of {html.escape(host['hostname'])}", + "", + f"

Status for {html.escape(host['hostname'])}

", + f"

Generated: {now}

", + # Host Overview (no disk) + "

Host Overview

", + f"", + f"" + f"", + f"", + "
Uptime{html.escape(host['uptime'])}
Load (1m,5m,15m){host['load1']}, {host['load5']}, {host['load15']}
Nextbox Package{html.escape(nextbox_version)}
", + # Services + "

Service Status

" + ] + for svc, st in services.items(): + cls = "error" if st.lower() != "active" else "" + parts.append( + f"" + f"" + ) + parts.append("
ServiceStatus
{html.escape(svc)}{html.escape(st)}
") + + # Journal Matches + parts.append("

Journal Pattern Checks

") + parts.append("") + for label, (ok, message) in journal_checks.items(): + cls = "" if ok else "error" + status = "OK" if ok else "ERROR" + parts.append( + f"" + f"" + f"" + f"" + f"" + ) + parts.append("
CheckStatusDetails
{html.escape(label)}{status}{html.escape(message)}
") + + return "\n".join(parts) + +# ------------------------------------------------------------------- +# MAIN LOOP + HTTP SERVER +# ------------------------------------------------------------------- + +def generate_page(): + host = get_host_info() + nb_ver = get_package_version("nextbox") + journal_checks = check_journal_boot(JOURNAL_PATTERNS) + svcs = check_services(SERVICES) + page = render_html(host, nb_ver, journal_checks, svcs) + + os.makedirs(os.path.dirname(OUTPUT_HTML), exist_ok=True) + tmp = OUTPUT_HTML + ".tmp" + with open(tmp, "w") as f: + f.write(page) + os.replace(tmp, OUTPUT_HTML) + print(f"[{datetime.datetime.now()}] Updated {OUTPUT_HTML}", flush=True) + +class StatusHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path in ("/", "/status.html"): + try: + with open(OUTPUT_HTML, "rb") as f: + data = f.read() + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + except FileNotFoundError: + self.send_error(404, "Status page not found") + except Exception as e: + self.send_error(500, f"Error: {e}") + else: + self.send_error(404, "Not Found") + +def serve(): + srv = HTTPServer(("0.0.0.0", HTTP_PORT), StatusHandler) + print(f"Serving on port {HTTP_PORT}", flush=True) + srv.serve_forever() + +def shutdown(signum, frame): + print("Shutting down...", flush=True) + sys.exit(0) + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, shutdown) + generate_page() + + # background refresher + def refresher(): + import time + while True: + time.sleep(REFRESH_INTERVAL) + generate_page() + threading.Thread(target=refresher, daemon=True).start() + + serve() + diff --git a/janitor/nextbox_status_page.service b/janitor/nextbox_status_page.service new file mode 100644 index 00000000..89719851 --- /dev/null +++ b/janitor/nextbox_status_page.service @@ -0,0 +1,14 @@ +[Unit] +Description=Static HTML Device Status + HTTP Server +After=network.target + +[Service] +Type=simple +User=root +ExecStart=/usr/bin/nextbox_status_page.py +Restart=on-failure +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target