From 79bb3c43f42e6dc7c6859bccbf7d763dbdf16e37 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 4 Feb 2026 21:02:46 +0100 Subject: [PATCH] feat: Add smbfs mount manager, Jellyfin READMEs, Glances host visibility, planning updates New secubox-app-smbfs package for SMB/CIFS remote directory management with smbfsctl CLI (add/remove/mount/umount/test/status), UCI config, auto-mount init script, and Jellyfin/Lyrion media path integration. Glances LXC: host bind mounts (/rom, /overlay, /boot, /srv), Docker socket fix (symlink loop), fs plugin @exit_after patch, hostname/OS identity, pre-generated /etc/mtab. KISS READMEs for secubox-app-jellyfin and luci-app-jellyfin. Planning files updated with Domoticz IoT, AI Gateway strategy, App Store P2P emancipation, and v2 roadmap items. Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 4 + .claude/TODO.md | 64 +++ .claude/WIP.md | 46 +- package/secubox/luci-app-jellyfin/README.md | 48 ++ .../files/etc/config/glances | 2 +- .../files/usr/sbin/glancesctl | 106 ++-- .../secubox/secubox-app-jellyfin/README.md | 93 ++++ package/secubox/secubox-app-smbfs/Makefile | 64 +++ package/secubox/secubox-app-smbfs/README.md | 101 ++++ .../secubox-app-smbfs/files/etc/config/smbfs | 21 + .../secubox-app-smbfs/files/etc/init.d/smbfs | 27 + .../secubox-app-smbfs/files/usr/sbin/smbfsctl | 504 ++++++++++++++++++ .../root/usr/share/secubox/catalog.json | 56 ++ 13 files changed, 1076 insertions(+), 60 deletions(-) create mode 100644 package/secubox/luci-app-jellyfin/README.md create mode 100644 package/secubox/secubox-app-jellyfin/README.md create mode 100644 package/secubox/secubox-app-smbfs/Makefile create mode 100644 package/secubox/secubox-app-smbfs/README.md create mode 100644 package/secubox/secubox-app-smbfs/files/etc/config/smbfs create mode 100644 package/secubox/secubox-app-smbfs/files/etc/init.d/smbfs create mode 100644 package/secubox/secubox-app-smbfs/files/usr/sbin/smbfsctl diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index a12187a1..73d7d45b 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -132,3 +132,7 @@ _Last updated: 2026-02-04_ - `streamlit`: Chunked upload to bypass uhttpd 64KB JSON limit, UTF-8 `.py` file upload fix, auto-install requirements from ZIP, non-standard filename support. - `crowdsec-dashboard`: Decisions list fix (wrong RPC expect key). - RPCD: BusyBox ash `local` keyword compatibility fix (wrap call handlers in function). + - `glances`: Full host system visibility — LXC bind mounts for `/rom`, `/overlay`, `/boot`, `/srv`, Docker socket at `/run/docker.sock` (symlink loop fix), `@exit_after` fs plugin patch (multiprocessing fails in LXC), host hostname via `lxc.uts.name`, OpenWrt OS identity from `/etc/openwrt_release`, pre-generated `/etc/mtab` from host `/proc/mounts`. + - `zigbee2mqtt`: Direct `/dev/ttyUSB0` passthrough (socat TCP bridge fails ASH protocol), adapter `ezsp`→`ember` (z2m 2.x), `ZIGBEE2MQTT_DATA` env var, `mosquitto-nossl` dependency. + - `smbfs`: New SMB/CIFS remote mount manager package — UCI config, `smbfsctl` CLI (add/remove/mount/umount/test/status), auto-mount init script, credentials storage, Jellyfin+Lyrion integration, catalog entry. + - `jellyfin`: KISS READMEs for both backend and LuCI packages. diff --git a/.claude/TODO.md b/.claude/TODO.md index e8c85c1b..978b0c49 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -5,6 +5,8 @@ _Last updated: 2026-02-04_ ## Resolved - ~~Expose cyberpunk option inside SecuBox Settings~~ — Done: `THEME_CHOICES` now includes `cyberpunk` in `settings.js`. +- ~~Glances full system monitoring~~ — Done: LXC host bind mounts, Docker socket, fs plugin patch, hostname/OS identity (2026-02-04). +- ~~Zigbee2MQTT dongle connection~~ — Done: adapter `ezsp`→`ember`, `ZIGBEE2MQTT_DATA` env var, direct `/dev/ttyUSB0` passthrough (2026-02-04). ## Open @@ -41,3 +43,65 @@ _Last updated: 2026-02-04_ 9. **Testing** - Capture screenshot baselines for dark/light/cyberpunk themes. - Automate browser cache busting (append `?v=` to view URLs). + +10. **SMB/CIFS Shared Remote Directories** + - Implement smbfs/cifs mount management for shared remote directories. + - Media handling: backups, sources, Lyrion music library, Jellyfin media paths. + - UCI config + LuCI UI for mount management (credentials, auto-mount, mount points). + - Integration hooks for media apps (Jellyfin, Lyrion, backup scripts). + +11. **Metablogizer Upload Failures** + - Investigate and fix failed file uploads in Metablogizer. + - May be related to uhttpd 64KB JSON limit (similar to Streamlit fix). + +12. **SecuBox v2 Roadmap & Objectives** + - EnigmaBox integration evaluation (community vote?). + - VoIP integration (SIP/WebRTC). + - Domoticz home automation integration. + - SSMTP / mail host / MX record management. + - Reverse MWAN WireGuard peers (multi-WAN failover over mesh). + - Nextcloud self-hosted cloud storage. + - Version v2 release planning and feature prioritization. + + **AI Management Layer** (ref: `SecuBox_LocalAI_Strategic_Analysis.html`): + - Phase 1 (v0.18): Upgrade LocalAI → 3.9, MCP Server, Threat Analyst agent, DNS Guard migration. + - Phase 2 (v0.19): CVE Triage + Network Anomaly agents, LocalRecall memory, AI Insights dashboard. + - Phase 3 (v1.0): Config Advisor (ANSSI prep), P2P Mesh Intelligence, Factory auto-provisioning. + - Hybrid approach: Ollama (inference) + LocalAI (orchestrator) + LocalAGI (agents) + LocalRecall (memory). + - MCP tools: crowdsec.alerts, waf.logs, dns.queries, network.flows, system.metrics, wireguard.status, uci.config. + + **AI Gateway Hybrid Architecture** (ref: `SecuBox_AI_Gateway_Hybrid_Architecture.html`): + - `secubox-ai-gateway` package: LiteLLM Proxy (port 4000) + Data Classifier + MCP Server. + - Data classification: LOCAL ONLY (raw network data) / SANITIZED (IPs scrubbed) / CLOUD DIRECT (generic). + - Providers: Mistral (EU sovereign, priority 1) > Claude > GPT > Gemini > xAI (all opt-in). + - Offline resilience: Local tier always active, cloud is bonus not dependency. + - Budget cap: configurable monthly cloud spend limit via LiteLLM. + - ANSSI CSPN: Data Classifier + Mistral EU + offline mode = triple sovereignty proof. + +13. **Punk Exposure Multi-Domain DNS** + - Multi-domain DNS with P2P exposure and Tor endpoints. + - Classical HTTPS endpoint (DNS provider API: OVH, Gandi, Cloudflare). + - Administrable DNS provider API integration via `dnsctl`. + - Mapped to local services, mesh-federated, locally tweakable. + - Follows Peek / Poke / Emancipate model (see `PUNK-EXPOSURE.md`). + +14. **Jellyfin Post-Install** + - Complete startup wizard (media library configuration). + - ~~README documentation~~ — Done (2026-02-04). + +15. **Domoticz IoT Integration & SecuBox Peering** + - Create dedicated `luci-app-domoticz` (currently no LuCI app — only generic vhost-manager). + - MQTT auto-bridge: auto-configure Domoticz ↔ zigbee2mqtt via Mosquitto broker. + - Zigbee device discovery: expose z2m device list in Domoticz setup wizard. + - SecuBox P2P mesh: register Domoticz as a mesh service (`secubox-p2p register-service`). + - Tor/DNS exposure channels: add to exposure scanner and Punk Exposure model. + - USB device passthrough: document `/srv/devices` for additional IoT dongles. + - Backup integration: include `/srv/domoticz/config` in secubox-recovery. + - Service registry: add Domoticz to `secubox-p2p` catalog and health checks. + +16. **App Store P2P Emancipation** + - Emancipate the app store WebUI as a remote P2P/torrent endpoint. + - Generative remote IPK distribution (like master-link dynamic join IPK generation). + - Decentralized package distribution across mesh nodes. + - Compatible with existing bonus-feed and secubox-feed infrastructure. + - Torrent-style swarming for large IPK downloads across mesh peers. diff --git a/.claude/WIP.md b/.claude/WIP.md index eba4398a..97675f9f 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -2,35 +2,39 @@ ## Active Threads +- **SMB/CIFS Remote Mount Manager** + Status: DONE — package created (2026-02-04) + Notes: New `secubox-app-smbfs` package with `smbfsctl` CLI, UCI config, init script, catalog entry. + Integrates with Jellyfin and Lyrion media paths. + +- **Jellyfin README** + Status: DONE (2026-02-04) + Notes: KISS READMEs created for both `secubox-app-jellyfin` and `luci-app-jellyfin`. + +- **Glances Full System Monitoring** + Status: COMPLETE (2026-02-04) + Notes: LXC host bind mounts, Docker socket, fs plugin patch, hostname/OS identity. + - **Zigbee2mqtt LXC Rewrite** Status: COMPLETE (2026-02-04) - Notes: Rewritten from Docker to LXC Alpine container. Feed rebuilt. - Deploy fix (2026-02-04): adapter `ezsp`→`ember` (z2m 2.x rename), added `ZIGBEE2MQTT_DATA` env var to start script, added `mosquitto-nossl` dependency. Direct `/dev/ttyUSB0` passthrough works; socat TCP bridge does NOT work (ASH RSTACK timeout). + Notes: Direct `/dev/ttyUSB0` passthrough, adapter `ezsp`→`ember`, `ZIGBEE2MQTT_DATA` env var. -- **Jellyfin Media Server** - Status: COMPLETE (2026-02-04) - Notes: New secubox-app-jellyfin + luci-app-jellyfin with LXC, HAProxy integration, uninstall/update/backup. +## Strategic Documents Received -- **Device Intel & DNS Provider** - Status: COMPLETE (2026-02-04) - Notes: New packages added. BusyBox compatibility, OUI emoji display, SDK build pattern aligned. - -- **Exposure KISS Redesign** - Status: COMPLETE (2026-02-04) - Notes: Enriched service names, vhost integration, DNS domain sorting, toggle switch fix. - -- **Streamlit Upload Fixes** - Status: COMPLETE (2026-02-04) - Notes: Chunked upload (uhttpd 64KB limit), UTF-8 fix, ZIP requirements auto-install, rename support. +- `SecuBox_LocalAI_Strategic_Analysis.html` — AI Management Layer roadmap (LocalAI 3.9 + LocalAGI + MCP). +- `SecuBox_AI_Gateway_Hybrid_Architecture.html` — Hybrid Local/Cloud architecture (LiteLLM + Data Classifier + multi-provider). ## Next Up -- Port the chip header layout to remaining SecuBox derivative apps (client-guardian, auth-guardian) — still pending, neither has `sh-page-header` pattern. -- Rebuild bonus feed with all 2026-02-04 changes (partially done — zigbee2mqtt and device-intel included, verify completeness). -- Commit uncommitted working tree changes (bonus-feed IPKs, zigbee2mqttctl). +1. **Domoticz IoT Integration** — LuCI app, MQTT auto-bridge, zigbee2mqtt integration, P2P mesh. +2. **Metablogizer Upload Fixes** — Investigate failed uploads. +3. **App Store P2P Emancipation** — Remote P2P/torrent endpoint, generative IPK distribution. +4. Port chip header layout to client-guardian, auth-guardian. +5. Rebuild bonus feed with all 2026-02-04 changes. +6. Commit uncommitted working tree changes (bonus-feed IPKs, glancesctl, zigbee2mqttctl, smbfs, jellyfin READMEs). ## Blockers / Risks -- Cyberpunk theme is now exposed in Settings UI (dark/light/system/cyberpunk) — previous blocker resolved. - No automated regression tests for LuCI views; manual verification required after each SCP deploy. -- `zigbee2mqttctl` has uncommitted changes in working tree. +- Glances + Zigbee2MQTT + SMB/CIFS source changes uncommitted in working tree. +- Strategic AI documents noted but not yet implemented (v0.18+ roadmap). diff --git a/package/secubox/luci-app-jellyfin/README.md b/package/secubox/luci-app-jellyfin/README.md new file mode 100644 index 00000000..e7515374 --- /dev/null +++ b/package/secubox/luci-app-jellyfin/README.md @@ -0,0 +1,48 @@ +# LuCI Jellyfin Dashboard + +Web interface for managing Jellyfin media server with real-time status, container controls, and integration management. + +## Installation + +```bash +opkg install luci-app-jellyfin +``` + +## Access + +LuCI menu: **Services -> Jellyfin** + +## Sections + +- **Service Status** -- Container state (running/stopped/not installed), uptime, Docker health, disk usage +- **Integration Status** -- HAProxy (disabled/pending/configured), Mesh P2P, Firewall WAN +- **Actions** -- Install, Start, Stop, Restart, Update, Backup, Uninstall, Open Web UI +- **Configuration** -- Port, image, data path, timezone, domain, HAProxy SSL, media paths, GPU transcoding, mesh toggle +- **Logs** -- Live container log viewer (last 50 lines) + +## RPCD Methods + +Backend: `luci.jellyfin` + +| Method | Description | +|--------|-------------| +| `status` | Full service status, config, and integrations | +| `start` | Start Jellyfin container | +| `stop` | Stop Jellyfin container | +| `restart` | Restart Jellyfin container | +| `install` | Pull image and create container | +| `uninstall` | Remove container and data | +| `update` | Pull latest image and recreate | +| `configure_haproxy` | Register HAProxy vhost | +| `backup` | Create config/data backup | +| `restore` | Restore from backup archive | +| `logs` | Fetch container logs | + +## Dependencies + +- `luci-base` +- `secubox-app-jellyfin` + +## License + +Apache-2.0 diff --git a/package/secubox/secubox-app-glances/files/etc/config/glances b/package/secubox/secubox-app-glances/files/etc/config/glances index 65a8d1ca..d4679170 100644 --- a/package/secubox/secubox-app-glances/files/etc/config/glances +++ b/package/secubox/secubox-app-glances/files/etc/config/glances @@ -5,7 +5,7 @@ config glances 'main' option api_port '61209' option web_host '0.0.0.0' option refresh_rate '3' - option memory_limit '128M' + option memory_limit '256M' config monitoring 'monitoring' option monitor_docker '1' diff --git a/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl b/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl index d0bf6462..ac2ab2f6 100644 --- a/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl +++ b/package/secubox/secubox-app-glances/files/usr/sbin/glancesctl @@ -164,6 +164,9 @@ lxc_create_docker_rootfs() { # Configure container echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" mkdir -p "$rootfs/var/log/glances" "$rootfs/etc/glances" "$rootfs/tmp" + mkdir -p "$rootfs/host" "$rootfs/rom" "$rootfs/overlay" "$rootfs/boot" "$rootfs/srv" + mkdir -p "$rootfs/run" + touch "$rootfs/run/docker.sock" # Ensure /bin/sh exists if [ ! -x "$rootfs/bin/sh" ]; then @@ -177,37 +180,46 @@ lxc_create_docker_rootfs() { fi fi - # Create startup script - cat > "$rootfs/opt/start-glances.sh" << 'START' -#!/bin/sh -export PATH="/usr/local/bin:/usr/bin:/bin:$PATH" -export PYTHONPATH="/app:$PYTHONPATH" -cd / + # Patch Glances: disable @exit_after on fs plugin (multiprocessing + # fails inside LXC containers, causing all disk_usage calls to return None) + local fs_init="$rootfs/app/glances/plugins/fs/__init__.py" + if [ -f "$fs_init" ]; then + sed -i 's/^@exit_after(/#@exit_after(/' "$fs_init" + log_info "Patched Glances fs plugin for LXC compatibility" + fi -# Setup hostname resolution (required for socket.gethostbyname to work) -REAL_HOSTNAME=$(hostname 2>/dev/null || echo glances) -cat > /etc/hosts << EOF -127.0.0.1 localhost $REAL_HOSTNAME glances -::1 localhost ip6-localhost ip6-loopback -EOF + # Generate /etc/mtab from host mounts (psutil reads this for disk_partitions) + grep -E '^(/dev/|overlayfs:)' /proc/mounts | \ + grep -v '/srv/docker/overlay' > "$rootfs/etc/mtab" 2>/dev/null -# Read environment variables -WEB_PORT="${GLANCES_WEB_PORT:-61208}" -WEB_HOST="${GLANCES_WEB_HOST:-0.0.0.0}" -REFRESH="${GLANCES_REFRESH:-3}" - -# Build args for web server mode -# Use -B for bind address and -p for port separately -# Disable autodiscover and check-update to avoid DNS resolution issues -ARGS="-w -B $WEB_HOST -p $WEB_PORT -t $REFRESH --disable-autodiscover --disable-check-update" - -# Disable plugins if configured -[ "$GLANCES_NO_DOCKER" = "1" ] && ARGS="$ARGS --disable-plugin docker" -[ "$GLANCES_NO_SENSORS" = "1" ] && ARGS="$ARGS --disable-plugin sensors" - -echo "Starting Glances web server on $WEB_HOST:$WEB_PORT..." -exec /venv/bin/python -m glances $ARGS -START + # Create startup script (written via file, not heredoc, to preserve shebang) + printf '%s\n' '#!/bin/sh' \ + 'export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"' \ + 'export PYTHONPATH="/app:$PYTHONPATH"' \ + 'cd /' \ + '' \ + '# Set hostname from host kernel (proc:mixed exposes this)' \ + 'REAL_HOSTNAME=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo glances)' \ + 'hostname "$REAL_HOSTNAME" 2>/dev/null' \ + 'printf "127.0.0.1\tlocalhost %s glances\n::1\t\tlocalhost ip6-localhost\n" "$REAL_HOSTNAME" > /etc/hosts' \ + '' \ + '# Copy host os-release so Glances reports OpenWrt, not Alpine' \ + 'if [ -f /host/openwrt_release ]; then' \ + " RELEASE=\$(grep DISTRIB_RELEASE /host/openwrt_release 2>/dev/null | cut -d\"'\" -f2)" \ + ' printf '"'"'NAME="OpenWrt"\nVERSION="%s"\nID=openwrt\nPRETTY_NAME="OpenWrt %s"\n'"'"' "$RELEASE" "$RELEASE" > /etc/os-release' \ + 'fi' \ + '' \ + 'WEB_PORT="${GLANCES_WEB_PORT:-61208}"' \ + 'WEB_HOST="${GLANCES_WEB_HOST:-0.0.0.0}"' \ + 'REFRESH="${GLANCES_REFRESH:-3}"' \ + '' \ + 'ARGS="-w -B $WEB_HOST -p $WEB_PORT -t $REFRESH --disable-autodiscover --disable-check-update"' \ + '[ "$GLANCES_NO_DOCKER" = "1" ] && ARGS="$ARGS --disable-plugin docker"' \ + '[ "$GLANCES_NO_SENSORS" = "1" ] && ARGS="$ARGS --disable-plugin sensors"' \ + '' \ + 'echo "Starting Glances on ${WEB_HOST}:${WEB_PORT} host=${REAL_HOSTNAME}..."' \ + 'exec /venv/bin/python -m glances $ARGS' \ + > "$rootfs/opt/start-glances.sh" chmod +x "$rootfs/opt/start-glances.sh" log_info "Glances Docker image extracted successfully" @@ -216,19 +228,33 @@ START lxc_create_config() { load_config - cat > "$LXC_CONFIG" << EOF -# Glances LXC Configuration -lxc.uts.name = $LXC_NAME + local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo glances) + local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/') -# Root filesystem + cat > "$LXC_CONFIG" << EOF +# Glances LXC Configuration — full host visibility +lxc.uts.name = $hostname lxc.rootfs.path = dir:$LXC_ROOTFS -# Network - use host network for full system visibility +# Network - host network for full visibility lxc.net.0.type = none -# Mounts - give access to host system info via proc:mixed +# Auto-mounts lxc.mount.auto = proc:mixed sys:ro cgroup:mixed +# Host filesystem bind mounts (read-only) for disk monitoring +lxc.mount.entry = /rom rom none bind,ro,create=dir 0 0 +lxc.mount.entry = /overlay overlay none bind,ro,create=dir 0 0 +lxc.mount.entry = /boot boot none bind,ro,create=dir 0 0 +lxc.mount.entry = /srv srv none bind,ro,create=dir 0 0 + +# Host info for OS identification +lxc.mount.entry = /etc/openwrt_release host/openwrt_release none bind,ro,create=file 0 0 +lxc.mount.entry = /etc/openwrt_version host/openwrt_version none bind,ro,create=file 0 0 + +# Docker socket for container monitoring (mount at /run, not /var/run which is a symlink) +lxc.mount.entry = /var/run/docker.sock run/docker.sock none bind,create=file 0 0 + # Environment variables lxc.environment = GLANCES_WEB_PORT=$web_port lxc.environment = GLANCES_WEB_HOST=$web_host @@ -236,11 +262,11 @@ lxc.environment = GLANCES_REFRESH=$refresh_rate lxc.environment = GLANCES_NO_DOCKER=$([ "$monitor_docker" = "0" ] && echo 1 || echo 0) lxc.environment = GLANCES_NO_SENSORS=$([ "$monitor_sensors" = "0" ] && echo 1 || echo 0) -# Capabilities - minimal for monitoring -lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_rawio +# Capabilities +lxc.cap.drop = sys_module mac_admin mac_override sys_rawio # cgroups limits -lxc.cgroup.memory.limit_in_bytes = $memory_limit +lxc.cgroup2.memory.max = $mem_bytes # Init lxc.init.cmd = /opt/start-glances.sh @@ -271,6 +297,10 @@ lxc_run() { # Regenerate config to pick up any UCI changes lxc_create_config + # Regenerate /etc/mtab from host mounts (psutil reads this) + grep -E '^(/dev/|overlayfs:)' /proc/mounts | \ + grep -v '/srv/docker/overlay' > "$LXC_ROOTFS/etc/mtab" 2>/dev/null + log_info "Starting Glances LXC container..." log_info "Web interface: http://0.0.0.0:$web_port" exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" diff --git a/package/secubox/secubox-app-jellyfin/README.md b/package/secubox/secubox-app-jellyfin/README.md new file mode 100644 index 00000000..1487ab9c --- /dev/null +++ b/package/secubox/secubox-app-jellyfin/README.md @@ -0,0 +1,93 @@ +# SecuBox Jellyfin Media Server + +Free media server for streaming movies, TV shows, music, and photos. Runs Jellyfin inside Docker on SecuBox-powered OpenWrt systems. + +## Installation + +```sh +opkg install secubox-app-jellyfin +jellyfinctl install +``` + +## Configuration + +UCI config file: `/etc/config/jellyfin` + +``` +config jellyfin 'main' + option enabled '0' + option image 'jellyfin/jellyfin:latest' + option data_path '/srv/jellyfin' + option port '8096' + option timezone 'Europe/Paris' + +config jellyfin 'media' + list media_path '/mnt/media/movies' + list media_path '/mnt/media/music' + +config jellyfin 'network' + option domain 'jellyfin.secubox.local' + option haproxy '0' + option firewall_wan '0' + +config jellyfin 'transcoding' + option hw_accel '0' + +config jellyfin 'mesh' + option enabled '0' +``` + +## Usage + +```sh +# Service control +/etc/init.d/jellyfin start +/etc/init.d/jellyfin stop + +# Controller CLI +jellyfinctl install # Pull Docker image and create container +jellyfinctl status # Show container and integration status +jellyfinctl update # Pull latest image and recreate container +jellyfinctl logs # Show container logs (-f to follow) +jellyfinctl shell # Open shell inside container +jellyfinctl backup # Backup config and data +jellyfinctl restore # Restore from backup archive +jellyfinctl uninstall # Stop and remove container and data + +# Integrations +jellyfinctl configure-haproxy # Register HAProxy vhost with SSL +jellyfinctl remove-haproxy # Remove HAProxy vhost +jellyfinctl configure-fw # Open WAN firewall port +jellyfinctl remove-fw # Close WAN firewall port +jellyfinctl register-mesh # Register in SecuBox P2P mesh +jellyfinctl unregister-mesh # Remove from mesh registry +``` + +Web UI: `http://:8096` + +## Features + +- Docker-based Jellyfin with full lifecycle management +- Multi-path media libraries (movies, music, photos, shows) +- Hardware GPU transcoding support +- HAProxy reverse proxy with SSL/ACME integration +- Firewall WAN port exposure +- SecuBox P2P mesh service registration +- Full config and data backup/restore +- Container shell access and log streaming + +## Files + +- `/etc/config/jellyfin` -- UCI configuration +- `/etc/init.d/jellyfin` -- procd service script +- `/usr/sbin/jellyfinctl` -- controller CLI + +## Dependencies + +- `dockerd` +- `docker` +- `containerd` + +## License + +Apache-2.0 diff --git a/package/secubox/secubox-app-smbfs/Makefile b/package/secubox/secubox-app-smbfs/Makefile new file mode 100644 index 00000000..9a0607f4 --- /dev/null +++ b/package/secubox/secubox-app-smbfs/Makefile @@ -0,0 +1,64 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-smbfs +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_ARCH:=all +PKG_MAINTAINER:=CyberMind Studio +PKG_LICENSE:=Apache-2.0 +PKG_FLAGS:=nonshared + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-smbfs + SECTION:=utils + CATEGORY:=Utilities + PKGARCH:=all + SUBMENU:=SecuBox Apps + TITLE:=SecuBox SMB/CIFS remote mount manager + DEPENDS:=+kmod-fs-cifs +cifsmount +endef + +define Package/secubox-app-smbfs/description +SMB/CIFS remote directory mount manager for SecuBox. Manages shared +network mounts for media servers (Jellyfin, Lyrion), backups, and +general-purpose remote storage over SMB/CIFS protocol. +endef + +define Package/secubox-app-smbfs/conffiles +/etc/config/smbfs +endef + +define Build/Compile +endef + +define Package/secubox-app-smbfs/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/smbfs $(1)/etc/config/smbfs + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/smbfs $(1)/etc/init.d/smbfs + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/smbfsctl $(1)/usr/sbin/smbfsctl +endef + +define Package/secubox-app-smbfs/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + echo "" + echo "============================================" + echo " SecuBox SMB/CIFS Mount Manager Installed" + echo "============================================" + echo "" + echo "Quick Start:" + echo " 1. Add share: smbfsctl add myshare //server/share /mnt/smb/myshare" + echo " 2. Set creds: smbfsctl credentials myshare user password" + echo " 3. Mount: smbfsctl mount myshare" + echo " 4. Auto-mount: smbfsctl enable myshare" + echo "" +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-smbfs)) diff --git a/package/secubox/secubox-app-smbfs/README.md b/package/secubox/secubox-app-smbfs/README.md new file mode 100644 index 00000000..ca3dfb0c --- /dev/null +++ b/package/secubox/secubox-app-smbfs/README.md @@ -0,0 +1,101 @@ +# SecuBox SMB/CIFS Remote Mount Manager + +Manages SMB/CIFS network shares for media servers (Jellyfin, Lyrion), backups, and general-purpose remote storage. + +## Installation + +```sh +opkg install secubox-app-smbfs +``` + +## Configuration + +UCI config file: `/etc/config/smbfs` + +``` +config smbfs 'global' + option enabled '1' + option mount_base '/mnt/smb' + option cifs_version '3.0' + option timeout '10' + +config mount 'movies' + option enabled '1' + option server '//192.168.1.100/movies' + option mountpoint '/mnt/smb/movies' + option username 'media' + option _password 'secret' + option read_only '1' + option auto_mount '1' + option description 'NAS movie library' +``` + +## Usage + +```sh +# Add a share +smbfsctl add movies //nas/movies /mnt/smb/movies + +# Set credentials +smbfsctl credentials movies user password + +# Set options +smbfsctl set movies read_only 1 +smbfsctl set movies description 'Movie library' + +# Test connectivity +smbfsctl test movies + +# Mount / unmount +smbfsctl mount movies +smbfsctl umount movies + +# Enable auto-mount at boot +smbfsctl enable movies + +# List all shares +smbfsctl list + +# Show detailed mount status +smbfsctl status + +# Mount all enabled shares +smbfsctl mount-all +``` + +## Integration with Media Apps + +```sh +# Jellyfin: add mounted share as media library +uci add_list jellyfin.media.media_path='/mnt/smb/movies' +uci commit jellyfin + +# Lyrion: point music library to mounted share +uci set lyrion.main.media_path='/mnt/smb/music' +uci commit lyrion +``` + +## Features + +- UCI-based share configuration with credentials storage +- Auto-mount at boot for enabled shares +- Read-only or read-write mount modes +- CIFS protocol version selection (2.0, 2.1, 3.0) +- Connectivity test before mounting +- Mount status with disk usage reporting +- Integration with Jellyfin and Lyrion media paths + +## Files + +- `/etc/config/smbfs` -- UCI configuration +- `/etc/init.d/smbfs` -- procd init script (auto-mount) +- `/usr/sbin/smbfsctl` -- controller CLI + +## Dependencies + +- `kmod-fs-cifs` -- CIFS kernel module +- `cifsmount` -- mount.cifs utility + +## License + +Apache-2.0 diff --git a/package/secubox/secubox-app-smbfs/files/etc/config/smbfs b/package/secubox/secubox-app-smbfs/files/etc/config/smbfs new file mode 100644 index 00000000..da674b7d --- /dev/null +++ b/package/secubox/secubox-app-smbfs/files/etc/config/smbfs @@ -0,0 +1,21 @@ +# SecuBox SMB/CIFS Remote Mount Manager +# Each 'mount' section defines a remote SMB share + +config smbfs 'global' + option enabled '1' + option mount_base '/mnt/smb' + option cifs_version '3.0' + option timeout '10' + +# Example share (disabled by default): +# config mount 'media' +# option enabled '0' +# option server '//192.168.1.100/media' +# option mountpoint '/mnt/smb/media' +# option username 'guest' +# option _password '' +# option domain '' +# option cifs_version '3.0' +# option read_only '1' +# option auto_mount '1' +# option description 'Media library share' diff --git a/package/secubox/secubox-app-smbfs/files/etc/init.d/smbfs b/package/secubox/secubox-app-smbfs/files/etc/init.d/smbfs new file mode 100644 index 00000000..2c3537a3 --- /dev/null +++ b/package/secubox/secubox-app-smbfs/files/etc/init.d/smbfs @@ -0,0 +1,27 @@ +#!/bin/sh /etc/rc.common +# SecuBox SMB/CIFS mount manager init script + +START=90 +STOP=15 +USE_PROCD=1 + +start_service() { + local enabled + enabled="$(uci -q get smbfs.global.enabled)" + [ "$enabled" = "1" ] || return 0 + + /usr/sbin/smbfsctl mount-all +} + +stop_service() { + /usr/sbin/smbfsctl umount-all +} + +reload_service() { + stop_service + start_service +} + +service_triggers() { + procd_add_reload_trigger "smbfs" +} diff --git a/package/secubox/secubox-app-smbfs/files/usr/sbin/smbfsctl b/package/secubox/secubox-app-smbfs/files/usr/sbin/smbfsctl new file mode 100644 index 00000000..206f683f --- /dev/null +++ b/package/secubox/secubox-app-smbfs/files/usr/sbin/smbfsctl @@ -0,0 +1,504 @@ +#!/bin/sh +# SecuBox SMB/CIFS remote mount manager +# Copyright (C) 2025-2026 CyberMind.fr + +CONFIG="smbfs" + +usage() { + cat <<'EOF' +Usage: smbfsctl [args] + +Share Management: + add Add a new SMB share + remove Remove a share definition + enable Enable auto-mount for a share + disable Disable auto-mount for a share + credentials Set credentials for a share + set Set a share option + list List all configured shares + +Mount Operations: + mount Mount a specific share + mount-all Mount all enabled shares + umount Unmount a specific share + umount-all Unmount all shares + status Show mount status of all shares + test Test connectivity to a share + +Examples: + smbfsctl add movies //nas/movies /mnt/smb/movies + smbfsctl credentials movies user mypass + smbfsctl set movies read_only 1 + smbfsctl mount movies + smbfsctl enable movies +EOF +} + +require_root() { [ "$(id -u)" -eq 0 ] || { echo "[ERROR] Root required" >&2; exit 1; }; } + +log_info() { echo "[INFO] $*"; } +log_warn() { echo "[WARN] $*" >&2; } +log_error() { echo "[ERROR] $*" >&2; } + +uci_get() { uci -q get ${CONFIG}.$1; } +uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } + +# Load global config +load_global() { + mount_base="$(uci_get global.mount_base || echo /mnt/smb)" + default_vers="$(uci_get global.cifs_version || echo 3.0)" + timeout="$(uci_get global.timeout || echo 10)" +} + +# Get list of configured share section names +get_shares() { + uci -q show "$CONFIG" | grep "=${CONFIG}\[" | sed "s/.*\.\(.*\)=.*/\1/" | sort -u + uci -q show "$CONFIG" | grep "=mount" | sed "s/${CONFIG}\.\(.*\)=mount/\1/" | sort -u +} + +# Check if share section exists +share_exists() { + local type + type="$(uci -q get ${CONFIG}.$1)" + [ "$type" = "mount" ] +} + +# Build mount options for a share +build_mount_opts() { + local name="$1" + local username password domain vers ro opts + + username="$(uci_get ${name}.username)" + password="$(uci_get ${name}._password)" + domain="$(uci_get ${name}.domain)" + vers="$(uci_get ${name}.cifs_version || echo "$default_vers")" + ro="$(uci_get ${name}.read_only)" + + opts="vers=${vers}" + + if [ -n "$username" ] && [ "$username" != "guest" ]; then + opts="${opts},username=${username}" + [ -n "$password" ] && opts="${opts},password=${password}" + [ -n "$domain" ] && opts="${opts},domain=${domain}" + else + opts="${opts},guest" + fi + + [ "$ro" = "1" ] && opts="${opts},ro" || opts="${opts},rw" + + # Reasonable defaults for network mounts + opts="${opts},iocharset=utf8,noperm,noserverino" + + echo "$opts" +} + +# Check if a share is currently mounted +is_mounted() { + local mountpoint="$1" + grep -q " ${mountpoint} cifs " /proc/mounts 2>/dev/null +} + +# ============================================================================= +# SHARE MANAGEMENT +# ============================================================================= + +cmd_add() { + local name="$1" server="$2" mountpoint="$3" + + [ -z "$name" ] || [ -z "$server" ] || [ -z "$mountpoint" ] && { + echo "Usage: smbfsctl add " >&2 + exit 1 + } + + if share_exists "$name"; then + log_error "Share '$name' already exists" + exit 1 + fi + + load_global + + uci add ${CONFIG} mount >/dev/null + # Rename the unnamed section to the given name + local idx + idx=$(uci -q show ${CONFIG} | grep "=mount$" | tail -1 | sed "s/${CONFIG}\.\(.*\)=mount/\1/") + uci rename ${CONFIG}.${idx}="${name}" + + uci set ${CONFIG}.${name}.enabled='0' + uci set ${CONFIG}.${name}.server="$server" + uci set ${CONFIG}.${name}.mountpoint="$mountpoint" + uci set ${CONFIG}.${name}.username='guest' + uci set ${CONFIG}.${name}._password='' + uci set ${CONFIG}.${name}.domain='' + uci set ${CONFIG}.${name}.cifs_version="$default_vers" + uci set ${CONFIG}.${name}.read_only='1' + uci set ${CONFIG}.${name}.auto_mount='0' + uci set ${CONFIG}.${name}.description='' + uci commit ${CONFIG} + + log_info "Share '$name' added: $server -> $mountpoint" + log_info "Set credentials: smbfsctl credentials $name " +} + +cmd_remove() { + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl remove " >&2; exit 1; } + + if ! share_exists "$name"; then + log_error "Share '$name' not found" + exit 1 + fi + + # Unmount first if mounted + local mountpoint + mountpoint="$(uci_get ${name}.mountpoint)" + if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then + umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null + fi + + uci delete ${CONFIG}.${name} + uci commit ${CONFIG} + + log_info "Share '$name' removed" +} + +cmd_enable() { + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl enable " >&2; exit 1; } + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + uci_set ${name}.enabled '1' + uci_set ${name}.auto_mount '1' + log_info "Share '$name' enabled for auto-mount" +} + +cmd_disable() { + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl disable " >&2; exit 1; } + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + uci_set ${name}.enabled '0' + uci_set ${name}.auto_mount '0' + log_info "Share '$name' disabled" +} + +cmd_credentials() { + local name="$1" user="$2" pass="$3" + + [ -z "$name" ] || [ -z "$user" ] && { + echo "Usage: smbfsctl credentials " >&2 + exit 1 + } + + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + uci_set ${name}.username "$user" + uci_set ${name}._password "$pass" + + log_info "Credentials set for '$name' (user: $user)" +} + +cmd_set() { + local name="$1" key="$2" value="$3" + + [ -z "$name" ] || [ -z "$key" ] && { + echo "Usage: smbfsctl set " >&2 + exit 1 + } + + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + uci_set ${name}.${key} "$value" + log_info "Set ${name}.${key} = $value" +} + +cmd_list() { + load_global + + local found=0 + local shares + shares="$(get_shares)" + + if [ -z "$shares" ]; then + echo "No SMB shares configured." + echo "Add one: smbfsctl add //server/share /mnt/smb/name" + return + fi + + printf "%-12s %-8s %-28s %-24s %s\n" "NAME" "STATUS" "SERVER" "MOUNTPOINT" "USER" + printf "%-12s %-8s %-28s %-24s %s\n" "----" "------" "------" "----------" "----" + + for name in $shares; do + local enabled server mountpoint username status + + enabled="$(uci_get ${name}.enabled)" + server="$(uci_get ${name}.server)" + mountpoint="$(uci_get ${name}.mountpoint)" + username="$(uci_get ${name}.username)" + + if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then + status="mounted" + elif [ "$enabled" = "1" ]; then + status="enabled" + else + status="disabled" + fi + + printf "%-12s %-8s %-28s %-24s %s\n" "$name" "$status" "$server" "$mountpoint" "${username:-guest}" + found=1 + done +} + +# ============================================================================= +# MOUNT OPERATIONS +# ============================================================================= + +cmd_mount() { + require_root + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl mount " >&2; exit 1; } + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + load_global + + local server mountpoint + server="$(uci_get ${name}.server)" + mountpoint="$(uci_get ${name}.mountpoint)" + + [ -z "$server" ] && { log_error "No server configured for '$name'"; exit 1; } + [ -z "$mountpoint" ] && { log_error "No mountpoint configured for '$name'"; exit 1; } + + if is_mounted "$mountpoint"; then + log_info "'$name' already mounted at $mountpoint" + return 0 + fi + + # Create mountpoint + mkdir -p "$mountpoint" + + local opts + opts="$(build_mount_opts "$name")" + + log_info "Mounting $server -> $mountpoint" + if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then + log_info "Mounted '$name' successfully" + else + log_error "Failed to mount '$name'" + return 1 + fi +} + +cmd_mount_all() { + require_root + load_global + + local shares count=0 fail=0 + shares="$(get_shares)" + + for name in $shares; do + local enabled auto_mount + enabled="$(uci_get ${name}.enabled)" + auto_mount="$(uci_get ${name}.auto_mount)" + + [ "$enabled" = "1" ] && [ "$auto_mount" = "1" ] || continue + + if cmd_mount_single "$name"; then + count=$((count + 1)) + else + fail=$((fail + 1)) + fi + done + + log_info "Mounted $count share(s), $fail failure(s)" +} + +# Internal: mount a single share (no arg validation) +cmd_mount_single() { + local name="$1" + local server mountpoint + + server="$(uci_get ${name}.server)" + mountpoint="$(uci_get ${name}.mountpoint)" + + [ -z "$server" ] || [ -z "$mountpoint" ] && return 1 + + if is_mounted "$mountpoint"; then + return 0 + fi + + mkdir -p "$mountpoint" + + local opts + opts="$(build_mount_opts "$name")" + + mount -t cifs "$server" "$mountpoint" -o "$opts" 2>/dev/null +} + +cmd_umount() { + require_root + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl umount " >&2; exit 1; } + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + local mountpoint + mountpoint="$(uci_get ${name}.mountpoint)" + + if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then + umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null + log_info "Unmounted '$name' from $mountpoint" + else + log_info "'$name' is not mounted" + fi +} + +cmd_umount_all() { + require_root + load_global + + local shares + shares="$(get_shares)" + + for name in $shares; do + local mountpoint + mountpoint="$(uci_get ${name}.mountpoint)" + if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then + umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null + log_info "Unmounted '$name'" + fi + done +} + +cmd_status() { + load_global + + echo "=== SMB/CIFS Mount Status ===" + echo "" + + local shares + shares="$(get_shares)" + + if [ -z "$shares" ]; then + echo "No shares configured." + return + fi + + for name in $shares; do + local enabled server mountpoint desc + + enabled="$(uci_get ${name}.enabled)" + server="$(uci_get ${name}.server)" + mountpoint="$(uci_get ${name}.mountpoint)" + desc="$(uci_get ${name}.description)" + + printf "Share: %s" "$name" + [ -n "$desc" ] && printf " (%s)" "$desc" + echo "" + + printf " Server: %s\n" "$server" + printf " Mountpoint: %s\n" "$mountpoint" + printf " Enabled: %s\n" "$([ "$enabled" = "1" ] && echo "yes" || echo "no")" + + if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then + # Get mount stats + local usage + usage=$(df -h "$mountpoint" 2>/dev/null | tail -1) + printf " Status: MOUNTED\n" + if [ -n "$usage" ]; then + local size used avail pct + size=$(echo "$usage" | awk '{print $2}') + used=$(echo "$usage" | awk '{print $3}') + avail=$(echo "$usage" | awk '{print $4}') + pct=$(echo "$usage" | awk '{print $5}') + printf " Disk: %s used / %s total (%s free, %s)\n" "$used" "$size" "$avail" "$pct" + fi + else + printf " Status: NOT MOUNTED\n" + fi + echo "" + done +} + +cmd_test() { + local name="$1" + [ -z "$name" ] && { echo "Usage: smbfsctl test " >&2; exit 1; } + share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } + + load_global + + local server + server="$(uci_get ${name}.server)" + + # Extract hostname from //host/share + local host + host=$(echo "$server" | sed 's|^//||; s|/.*||') + + log_info "Testing connectivity to $host..." + + # Test network reachability + if ping -c 1 -W "$timeout" "$host" >/dev/null 2>&1; then + log_info "Host $host is reachable" + else + log_error "Host $host is not reachable" + return 1 + fi + + # Test SMB port (445) + local smb_ok=0 + if [ -f /proc/net/tcp ]; then + # Try a TCP connection via shell + if (echo > /dev/tcp/"$host"/445) 2>/dev/null; then + smb_ok=1 + fi + fi + + # Fallback: try netstat or just attempt mount + if [ "$smb_ok" = "1" ]; then + log_info "SMB port 445 is open on $host" + else + log_warn "Could not verify SMB port 445 (will attempt mount anyway)" + fi + + # Try a test mount + require_root + local mountpoint="/tmp/smbfs-test-$$" + mkdir -p "$mountpoint" + + local opts + opts="$(build_mount_opts "$name")" + + if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then + log_info "Test mount successful — share is accessible" + local count + count=$(ls -1 "$mountpoint" 2>/dev/null | wc -l) + log_info "Contents: $count items visible" + umount "$mountpoint" 2>/dev/null + else + log_error "Test mount failed — check server, credentials, or share name" + rmdir "$mountpoint" 2>/dev/null + return 1 + fi + + rmdir "$mountpoint" 2>/dev/null + log_info "Test complete: share '$name' is working" +} + +# ============================================================================= +# MAIN +# ============================================================================= + +case "${1:-}" in + add) shift; cmd_add "$@" ;; + remove) shift; cmd_remove "$@" ;; + enable) shift; cmd_enable "$@" ;; + disable) shift; cmd_disable "$@" ;; + credentials) shift; cmd_credentials "$@" ;; + set) shift; cmd_set "$@" ;; + list) shift; cmd_list "$@" ;; + mount) shift; cmd_mount "$@" ;; + mount-all) shift; cmd_mount_all "$@" ;; + umount) shift; cmd_umount "$@" ;; + umount-all) shift; cmd_umount_all "$@" ;; + status) shift; cmd_status "$@" ;; + test) shift; cmd_test "$@" ;; + help|--help|-h|'') usage ;; + *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; +esac diff --git a/package/secubox/secubox-core/root/usr/share/secubox/catalog.json b/package/secubox/secubox-core/root/usr/share/secubox/catalog.json index 5abf1b9e..a292d905 100644 --- a/package/secubox/secubox-core/root/usr/share/secubox/catalog.json +++ b/package/secubox/secubox-core/root/usr/share/secubox/catalog.json @@ -2393,6 +2393,62 @@ "refresh_interval": 30, "metrics": [] } + }, + { + "id": "secubox-app-smbfs", + "name": "SMB/CIFS Mounts", + "version": "1.0.0", + "category": "system", + "runtime": "native", + "description": "SMB/CIFS remote directory mount manager for media servers, backups, and shared storage", + "author": "CyberMind.fr", + "license": "Apache-2.0", + "icon": "\ud83d\udcc1", + "tags": [ + "storage", + "smb", + "cifs", + "nas", + "media", + "backup" + ], + "packages": { + "required": [ + "secubox-app-smbfs", + "kmod-fs-cifs", + "cifsmount" + ] + }, + "capabilities": [ + "storage", + "nas", + "media-library" + ], + "requirements": { + "min_ram_mb": 16, + "min_storage_mb": 1 + }, + "status": "stable", + "notes": "Manages SMB/CIFS network mounts. Integrates with Jellyfin and Lyrion media paths.", + "pkg_version": "1.0.0-1", + "app_version": "1.0.0", + "changelog": { + "1.0.0": { + "date": "2026-02-04", + "changes": [ + "Initial release", + "UCI-based share management", + "Auto-mount at boot", + "Jellyfin and Lyrion integration" + ] + } + }, + "widget": { + "enabled": false, + "template": "default", + "refresh_interval": 60, + "metrics": [] + } } ], "featured_sections": {