mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 16:31:31 +00:00
Compare commits
12 Commits
b62adc5421
...
8905228cbd
| Author | SHA1 | Date | |
|---|---|---|---|
| 8905228cbd | |||
|
|
9a0a9873a7 | ||
|
|
c2f1682c59 | ||
| 382b4cb7dc | |||
| 33f6c1f68e | |||
| d58460ebaf | |||
|
|
4f19c604c7 | ||
|
|
81168ff49a | ||
|
|
199e52b5cb | ||
|
|
b97e36cdeb | ||
|
|
7c273e2132 | ||
|
|
986b18b163 |
|
|
@ -90,6 +90,7 @@ while [[ $# -gt 0 ]]; do
|
||||||
--size) IMG_SIZE="$2"; shift 2 ;;
|
--size) IMG_SIZE="$2"; shift 2 ;;
|
||||||
--embed-image) EMBED_IMAGE="$2"; shift 2 ;;
|
--embed-image) EMBED_IMAGE="$2"; shift 2 ;;
|
||||||
--local-cache) USE_LOCAL_CACHE=1; shift ;;
|
--local-cache) USE_LOCAL_CACHE=1; shift ;;
|
||||||
|
--slipstream) SLIPSTREAM_DEBS=1; shift ;;
|
||||||
--no-slipstream) SLIPSTREAM_DEBS=0; shift ;;
|
--no-slipstream) SLIPSTREAM_DEBS=0; shift ;;
|
||||||
--no-compress) NO_COMPRESS=1; shift ;;
|
--no-compress) NO_COMPRESS=1; shift ;;
|
||||||
--no-led) INCLUDE_LED_KERNEL=0; shift ;;
|
--no-led) INCLUDE_LED_KERNEL=0; shift ;;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,14 @@
|
||||||
|
secubox-droplet (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Forge dropletctl (issue #181) — third routing verb on the publishing
|
||||||
|
layer, parallel to giteactl repo mirror (#176) and mitmproxyctl route
|
||||||
|
(#173). Subcommands: lifecycle (start/stop/restart/status/logs),
|
||||||
|
Three-fold JSON (components/access), and file noun verbs (upload,
|
||||||
|
remove, rename, list, info) wrapping the /api/v1/droplet/* endpoints
|
||||||
|
over the Unix socket.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:20:54 +0200
|
||||||
|
|
||||||
secubox-droplet (1.0.1-1~bookworm1) bookworm; urgency=medium
|
secubox-droplet (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add dynamic menu system with menu.d JSON definitions
|
* Add dynamic menu system with menu.d JSON definitions
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-droplet/etc/nginx/secubox.d
|
install -d debian/secubox-droplet/etc/nginx/secubox.d
|
||||||
[ -f nginx/droplet.conf ] && cp nginx/droplet.conf debian/secubox-droplet/etc/nginx/secubox.d/ || true
|
[ -f nginx/droplet.conf ] && cp nginx/droplet.conf debian/secubox-droplet/etc/nginx/secubox.d/ || true
|
||||||
|
# dropletctl (issue #181)
|
||||||
|
install -d debian/secubox-droplet/usr/sbin
|
||||||
|
install -m 755 sbin/dropletctl debian/secubox-droplet/usr/sbin/
|
||||||
|
|
|
||||||
225
packages/secubox-droplet/sbin/dropletctl
Normal file
225
packages/secubox-droplet/sbin/dropletctl
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||||
|
# See LICENCE-CMSD-1.0.md for terms.
|
||||||
|
#
|
||||||
|
# dropletctl — SecuBox Droplet File Publisher control (issue #181)
|
||||||
|
#
|
||||||
|
# Third routing verb on the publishing layer, parallel to:
|
||||||
|
# haproxyctl vhost add/remove (routing)
|
||||||
|
# mitmproxyctl route add/remove (interception, #173)
|
||||||
|
# giteactl repo mirror add (replication, #176)
|
||||||
|
# dropletctl file upload/list (publishing, this)
|
||||||
|
#
|
||||||
|
# The Droplet API exposes /upload, /list, /remove, /rename over a Unix
|
||||||
|
# socket; this ctl wraps those endpoints under a coherent <noun> <verb>
|
||||||
|
# grammar so operators don't have to curl by hand.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SOCKET="${DROPLET_SOCKET:-/run/secubox/droplet.sock}"
|
||||||
|
API_BASE="http://localhost/api/v1/droplet"
|
||||||
|
SERVICE="secubox-droplet.service"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
log() { printf "${GREEN}[DROPLET]${NC} %s\n" "$*"; }
|
||||||
|
warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
|
||||||
|
error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; }
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_socket() {
|
||||||
|
if [ ! -S "$SOCKET" ]; then
|
||||||
|
error "API socket $SOCKET not present — start $SERVICE first"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1" path="$2"
|
||||||
|
shift 2
|
||||||
|
require_socket
|
||||||
|
curl --unix-socket "$SOCKET" -sS -X "$method" \
|
||||||
|
-w "\nHTTP_CODE:%{http_code}\n" \
|
||||||
|
"${API_BASE}${path}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_code() {
|
||||||
|
echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2
|
||||||
|
}
|
||||||
|
|
||||||
|
api_body() {
|
||||||
|
echo "$1" | sed '/^HTTP_CODE:/d'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Lifecycle (top-level, mirrors mitmproxyctl/giteactl) ──────────────────
|
||||||
|
|
||||||
|
cmd_install() { log "install via dpkg; this ctl assumes package already installed"; return 0; }
|
||||||
|
cmd_start() { systemctl start "$SERVICE" && log "started"; }
|
||||||
|
cmd_stop() { systemctl stop "$SERVICE" && log "stopped"; }
|
||||||
|
cmd_restart() { systemctl restart "$SERVICE" && log "restarted";}
|
||||||
|
cmd_status() {
|
||||||
|
systemctl is-active "$SERVICE" >/dev/null 2>&1 \
|
||||||
|
&& echo "active" || echo "inactive"
|
||||||
|
}
|
||||||
|
cmd_logs() {
|
||||||
|
journalctl -u "$SERVICE" -n "${1:-50}" --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Three-fold (giteactl convention: components / status / access JSON) ───
|
||||||
|
|
||||||
|
cmd_components() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"service": "$SERVICE",
|
||||||
|
"socket": "$SOCKET",
|
||||||
|
"api_base": "$API_BASE",
|
||||||
|
"ctl_version": "$VERSION"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_access() {
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"socket": "$SOCKET",
|
||||||
|
"api_paths": {
|
||||||
|
"upload": "POST /api/v1/droplet/upload",
|
||||||
|
"list": "GET /api/v1/droplet/list",
|
||||||
|
"remove": "POST /api/v1/droplet/remove",
|
||||||
|
"rename": "POST /api/v1/droplet/rename",
|
||||||
|
"info": "GET /api/v1/droplet/droplet/{name}",
|
||||||
|
"stats": "GET /api/v1/droplet/stats",
|
||||||
|
"storage":"GET /api/v1/droplet/storage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── file noun verbs (the missing grammar — issue #181) ────────────────────
|
||||||
|
|
||||||
|
cmd_file() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
upload) cmd_file_upload "$@" ;;
|
||||||
|
remove|rm|del) cmd_file_remove "$@" ;;
|
||||||
|
rename) cmd_file_rename "$@" ;;
|
||||||
|
list|ls) cmd_file_list "$@" ;;
|
||||||
|
info) cmd_file_info "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
File commands:
|
||||||
|
file upload <path> [--public] [--ttl <e.g. 7d>]
|
||||||
|
file remove <name>
|
||||||
|
file rename <old> <new>
|
||||||
|
file list [--limit N]
|
||||||
|
file info <name>
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_upload() {
|
||||||
|
local path="$1"; shift || { error "file upload <path> required"; return 1; }
|
||||||
|
[ -f "$path" ] || { error "file not found: $path"; return 1; }
|
||||||
|
local public=false ttl=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--public) public=true ;;
|
||||||
|
--ttl) ttl="$2"; shift ;;
|
||||||
|
*) error "unknown flag: $1"; return 1 ;;
|
||||||
|
esac; shift
|
||||||
|
done
|
||||||
|
log "uploading $path (public=$public ttl=${ttl:-default})"
|
||||||
|
local args=("-F" "file=@${path}")
|
||||||
|
$public && args+=("-F" "public=true")
|
||||||
|
[ -n "$ttl" ] && args+=("-F" "ttl=$ttl")
|
||||||
|
local out
|
||||||
|
out=$(api POST "/upload" "${args[@]}")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
case "$code" in
|
||||||
|
200|201) api_body "$out" ;;
|
||||||
|
*) error "upload failed (HTTP $code): $(api_body "$out" | head -3)"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_remove() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "file remove <name> required"; return 1; }
|
||||||
|
log "removing $name"
|
||||||
|
local out
|
||||||
|
out=$(api POST "/remove" -H "Content-Type: application/json" \
|
||||||
|
-d "$(printf '{"name":"%s"}' "$name")")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
[ "$code" = "200" ] || { error "remove failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||||
|
log "removed $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_rename() {
|
||||||
|
local old="$1" new="$2"
|
||||||
|
[ -z "$old" ] || [ -z "$new" ] && { error "file rename <old> <new> required"; return 1; }
|
||||||
|
log "renaming $old -> $new"
|
||||||
|
local out
|
||||||
|
out=$(api POST "/rename" -H "Content-Type: application/json" \
|
||||||
|
-d "$(printf '{"old":"%s","new":"%s"}' "$old" "$new")")
|
||||||
|
local code; code=$(api_code "$out")
|
||||||
|
[ "$code" = "200" ] || { error "rename failed (HTTP $code): $(api_body "$out")"; return 1; }
|
||||||
|
log "renamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_list() {
|
||||||
|
local limit=""
|
||||||
|
[ "${1:-}" = "--limit" ] && { limit="?limit=$2"; }
|
||||||
|
local out
|
||||||
|
out=$(api GET "/list${limit}")
|
||||||
|
api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_file_info() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "file info <name> required"; return 1; }
|
||||||
|
local out
|
||||||
|
out=$(api GET "/droplet/${name}")
|
||||||
|
api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main dispatch ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox Droplet Controller v$VERSION (issue #181)
|
||||||
|
File publisher CLI — parallel to giteactl, mitmproxyctl
|
||||||
|
|
||||||
|
Usage: dropletctl <command> [options]
|
||||||
|
|
||||||
|
Lifecycle:
|
||||||
|
install / start / stop / restart / status / logs
|
||||||
|
|
||||||
|
Three-fold (JSON):
|
||||||
|
components / access
|
||||||
|
|
||||||
|
Files (issue #181):
|
||||||
|
file upload <path> [--public] [--ttl 7d]
|
||||||
|
file remove <name>
|
||||||
|
file rename <old> <new>
|
||||||
|
file list [--limit N]
|
||||||
|
file info <name>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dropletctl file upload /tmp/report.pdf --public --ttl 7d
|
||||||
|
dropletctl file list
|
||||||
|
dropletctl access | jq .api_paths
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
install|start|stop|restart|status) cmd="$1"; shift; cmd_$cmd "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
components) cmd_components ;;
|
||||||
|
access) cmd_access ;;
|
||||||
|
file) shift; cmd_file "$@" ;;
|
||||||
|
help|--help|-h|'') show_help ;;
|
||||||
|
*) error "unknown command: $1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
@ -1,3 +1,21 @@
|
||||||
|
secubox-gitea (1.4.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* giteactl: forge `runner` noun verbs (issue #190) — the CI execution
|
||||||
|
layer of the SecuBox grammar, parallel to `repo mirror` (#176) and
|
||||||
|
`user`. Subcommands:
|
||||||
|
runner token gen
|
||||||
|
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||||
|
runner remove NAME [--keep-data]
|
||||||
|
runner list
|
||||||
|
runner logs NAME [--lines N]
|
||||||
|
runner restart NAME
|
||||||
|
Each runner lives in its own LXC `act-runner-<name>` (operator
|
||||||
|
decree: LXC only, no docker, no host-mode). `runner add` bootstraps
|
||||||
|
the LXC, downloads gitea-runner v1.0.3 from gitea.com, registers
|
||||||
|
against the local Gitea, installs a systemd unit, starts it.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:51:30 +0200
|
||||||
|
|
||||||
secubox-gitea (1.4.0-1~bookworm1) bookworm; urgency=medium
|
secubox-gitea (1.4.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Fix startup script: add PATH and HOME environment variables
|
* Fix startup script: add PATH and HOME environment variables
|
||||||
|
|
|
||||||
|
|
@ -672,6 +672,277 @@ except Exception as e:
|
||||||
" 2>&1
|
" 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Runner — the CI execution verb (issue #190)
|
||||||
|
#
|
||||||
|
# Parallel to `repo mirror` (#176) and `user` (existing). Bootstraps a
|
||||||
|
# dedicated LXC for each gitea-runner instance — operator decree: LXC only,
|
||||||
|
# no docker on host, no insecure host-mode.
|
||||||
|
#
|
||||||
|
# Subcommands:
|
||||||
|
# runner token gen Generate a one-shot registration token
|
||||||
|
# runner add NAME --labels L1,L2,... [--arch arm64|amd64] [--memory 1G]
|
||||||
|
# runner remove NAME [--keep-data]
|
||||||
|
# runner list
|
||||||
|
# runner logs NAME [--lines N]
|
||||||
|
# runner restart NAME
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
RUNNER_VERSION="${RUNNER_VERSION:-1.0.3}"
|
||||||
|
RUNNER_DL_BASE="https://gitea.com/gitea/runner/releases/download/v${RUNNER_VERSION}"
|
||||||
|
RUNNER_LXC_PREFIX="act-runner-"
|
||||||
|
RUNNER_GITEA_URL="${RUNNER_GITEA_URL:-http://${LXC_IP}:${HTTP_PORT}}"
|
||||||
|
RUNNER_BRIDGE="${RUNNER_BRIDGE:-br-lxc}"
|
||||||
|
RUNNER_SUBNET="${RUNNER_SUBNET:-10.100.0.0/24}"
|
||||||
|
RUNNER_GATEWAY="${RUNNER_GATEWAY:-10.100.0.1}"
|
||||||
|
RUNNER_IP_POOL_START="${RUNNER_IP_POOL_START:-50}" # 10.100.0.50..200
|
||||||
|
RUNNER_IP_POOL_END="${RUNNER_IP_POOL_END:-200}"
|
||||||
|
RUNNER_DNS="${RUNNER_DNS:-1.1.1.1 8.8.8.8}"
|
||||||
|
|
||||||
|
# Find the next free IP in the runner pool (10.100.0.50..200), avoiding
|
||||||
|
# IPs already declared in existing LXC configs.
|
||||||
|
_runner_next_ip() {
|
||||||
|
local octet ip used
|
||||||
|
used=$(grep -h "lxc.net.0.ipv4.address" "$LXC_PATH"/*/config 2>/dev/null \
|
||||||
|
| sed 's|.*= *\([0-9.]*\)/.*|\1|' \
|
||||||
|
| grep "^10\.100\.0\." | sed 's|.*\.||' | sort -un)
|
||||||
|
for octet in $(seq "$RUNNER_IP_POOL_START" "$RUNNER_IP_POOL_END"); do
|
||||||
|
echo "$used" | grep -qx "$octet" || { echo "10.100.0.$octet"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_runner_lxc_name() { echo "${RUNNER_LXC_PREFIX}${1}"; }
|
||||||
|
_runner_lxc_exists() { [ -d "$LXC_PATH/$(_runner_lxc_name "$1")/rootfs" ]; }
|
||||||
|
_runner_lxc_running() {
|
||||||
|
lxc-info -n "$(_runner_lxc_name "$1")" -P "$LXC_PATH" 2>/dev/null | grep -q "State:.*RUNNING"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate a runner registration token via gitea CLI inside the Gitea LXC.
|
||||||
|
# Output is the raw token on the last line.
|
||||||
|
cmd_runner_token_gen() {
|
||||||
|
require_root
|
||||||
|
if ! lxc_running; then error "gitea LXC not running"; return 1; fi
|
||||||
|
local out
|
||||||
|
out=$(lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- \
|
||||||
|
su -s /bin/bash gitea -c \
|
||||||
|
"gitea --config $GITEA_APP_INI actions generate-runner-token 2>&1" \
|
||||||
|
| tail -1)
|
||||||
|
[ -z "$out" ] && { error "token gen failed"; return 1; }
|
||||||
|
echo "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner_add() {
|
||||||
|
local name="$1"; shift || { error "runner add NAME required"; return 1; }
|
||||||
|
local labels="" arch="" memory="1G" ip=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--labels) labels="$2"; shift ;;
|
||||||
|
--arch) arch="$2"; shift ;;
|
||||||
|
--memory) memory="$2"; shift ;;
|
||||||
|
--ip) ip="$2"; shift ;;
|
||||||
|
*) error "unknown flag: $1"; return 1 ;;
|
||||||
|
esac; shift
|
||||||
|
done
|
||||||
|
[ -z "$labels" ] && { error "--labels L1,L2,... required"; return 1; }
|
||||||
|
[ -z "$arch" ] && arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
||||||
|
[ -z "$ip" ] && ip=$(_runner_next_ip)
|
||||||
|
[ -z "$ip" ] && { error "no free IP in pool 10.100.0.${RUNNER_IP_POOL_START}..${RUNNER_IP_POOL_END}"; return 1; }
|
||||||
|
|
||||||
|
require_root
|
||||||
|
local lxc_name; lxc_name=$(_runner_lxc_name "$name")
|
||||||
|
if _runner_lxc_exists "$name"; then
|
||||||
|
error "LXC $lxc_name already exists (use 'runner remove $name' first)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "creating LXC $lxc_name (arch $arch, memory $memory, ip $ip)"
|
||||||
|
lxc-create -n "$lxc_name" -P "$LXC_PATH" -t download -- \
|
||||||
|
-d debian -r bookworm -a "$arch"
|
||||||
|
# Static IP on br-lxc + memory limit (no DHCP on this bridge)
|
||||||
|
cat >> "$LXC_PATH/$lxc_name/config" <<EOF
|
||||||
|
|
||||||
|
# act-runner LXC — set via giteactl runner add (#190)
|
||||||
|
lxc.net.0.ipv4.address = ${ip}/24
|
||||||
|
lxc.net.0.ipv4.gateway = ${RUNNER_GATEWAY}
|
||||||
|
lxc.cgroup2.memory.max = $((${memory%[GgMm]} * (1024**$( [ "${memory: -1}" = "G" ] && echo 3 || echo 2 ))))
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "starting $lxc_name for bootstrap"
|
||||||
|
lxc-start -n "$lxc_name" -P "$LXC_PATH"
|
||||||
|
sleep 4 # let networking settle
|
||||||
|
|
||||||
|
# Fix DNS — Debian template's resolv.conf symlinks systemd-resolved
|
||||||
|
# stub which is empty until configured; write static nameservers.
|
||||||
|
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||||
|
for ns in $RUNNER_DNS; do echo \"nameserver \$ns\"; done > /etc/resolv.conf
|
||||||
|
"
|
||||||
|
|
||||||
|
log "installing gitea-runner v$RUNNER_VERSION inside"
|
||||||
|
local dl_url="${RUNNER_DL_BASE}/gitea-runner-${RUNNER_VERSION}-linux-${arch}"
|
||||||
|
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||||
|
set -e
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq curl ca-certificates git
|
||||||
|
curl -sL -o /usr/local/bin/act_runner '$dl_url'
|
||||||
|
chmod 755 /usr/local/bin/act_runner
|
||||||
|
useradd -r -s /usr/sbin/nologin -m -d /var/lib/act_runner act_runner 2>/dev/null || true
|
||||||
|
mkdir -p /etc/act_runner /var/lib/act_runner
|
||||||
|
chown -R act_runner:act_runner /var/lib/act_runner
|
||||||
|
"
|
||||||
|
|
||||||
|
log "generating runner registration token"
|
||||||
|
local token
|
||||||
|
token=$(cmd_runner_token_gen | tail -1)
|
||||||
|
[ -z "$token" ] && { error "could not get registration token"; return 1; }
|
||||||
|
|
||||||
|
log "registering runner $name (labels: $labels) -> $RUNNER_GITEA_URL"
|
||||||
|
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||||
|
cd /var/lib/act_runner
|
||||||
|
su -s /bin/bash act_runner -c \"
|
||||||
|
cd /var/lib/act_runner
|
||||||
|
act_runner register \\
|
||||||
|
--no-interactive \\
|
||||||
|
--instance '$RUNNER_GITEA_URL' \\
|
||||||
|
--token '$token' \\
|
||||||
|
--name '$name' \\
|
||||||
|
--labels '$labels'
|
||||||
|
\"
|
||||||
|
"
|
||||||
|
|
||||||
|
log "installing systemd unit + starting"
|
||||||
|
lxc-attach -n "$lxc_name" -P "$LXC_PATH" -- bash -c "
|
||||||
|
cat > /etc/systemd/system/act_runner.service <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea act_runner
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=act_runner
|
||||||
|
Group=act_runner
|
||||||
|
WorkingDirectory=/var/lib/act_runner
|
||||||
|
ExecStart=/usr/local/bin/act_runner daemon
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now act_runner.service
|
||||||
|
sleep 2
|
||||||
|
systemctl is-active act_runner.service
|
||||||
|
"
|
||||||
|
log "runner $name installed in $lxc_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner_remove() {
|
||||||
|
local name="$1"; shift || { error "runner remove NAME required"; return 1; }
|
||||||
|
local keep_data=false
|
||||||
|
[ "${1:-}" = "--keep-data" ] && keep_data=true
|
||||||
|
require_root
|
||||||
|
local lxc_name; lxc_name=$(_runner_lxc_name "$name")
|
||||||
|
if ! _runner_lxc_exists "$name"; then
|
||||||
|
warn "LXC $lxc_name does not exist; nothing to remove"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if _runner_lxc_running "$name"; then
|
||||||
|
log "stopping $lxc_name"
|
||||||
|
lxc-stop -n "$lxc_name" -P "$LXC_PATH"
|
||||||
|
fi
|
||||||
|
if $keep_data; then
|
||||||
|
log "keeping LXC rootfs (--keep-data); just stopped"
|
||||||
|
else
|
||||||
|
log "destroying $lxc_name"
|
||||||
|
lxc-destroy -n "$lxc_name" -P "$LXC_PATH"
|
||||||
|
fi
|
||||||
|
log "(note: runner remains registered in Gitea — visit Settings -> Actions -> Runners to delete the dead entry, or wait for it to expire)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner_list() {
|
||||||
|
require_root
|
||||||
|
echo "Local LXC runners:"
|
||||||
|
local any=0
|
||||||
|
for d in "$LXC_PATH"/${RUNNER_LXC_PREFIX}*; do
|
||||||
|
[ -d "$d/rootfs" ] || continue
|
||||||
|
any=1
|
||||||
|
local n; n=$(basename "$d" | sed "s|^${RUNNER_LXC_PREFIX}||")
|
||||||
|
local state="STOPPED"
|
||||||
|
_runner_lxc_running "$n" && state="RUNNING"
|
||||||
|
printf " %-25s %s\n" "$n" "$state"
|
||||||
|
done
|
||||||
|
[ $any = 0 ] && echo " (none)"
|
||||||
|
echo
|
||||||
|
echo "Registered runners (gitea API):"
|
||||||
|
local tok tname; read -r tok tname <<<"$(gitea_token_gen)" || return 1
|
||||||
|
local out
|
||||||
|
out=$(gitea_api "$tok" GET "/admin/runners" "")
|
||||||
|
gitea_token_revoke "$tname"
|
||||||
|
api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
|
||||||
|
echo "$out" | sed '/^HTTP_CODE:/d' | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
for r in d.get('runners', d if isinstance(d, list) else []):
|
||||||
|
print(f\" {r.get('name','?'):25s} status={r.get('status','?')} labels={','.join(r.get('labels',[]))}\")
|
||||||
|
if not (d.get('runners') if isinstance(d, dict) else d):
|
||||||
|
print(' (none registered)')
|
||||||
|
except Exception as e:
|
||||||
|
print(f' parse error: {e}')
|
||||||
|
" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner_logs() {
|
||||||
|
local name="$1"; [ -z "$name" ] && { error "runner logs NAME required"; return 1; }
|
||||||
|
local lines="${2:-100}"
|
||||||
|
[ "$1" = "--lines" ] && { lines="$2"; name="$3"; }
|
||||||
|
require_root
|
||||||
|
_runner_lxc_running "$name" || { error "LXC $(_runner_lxc_name "$name") not running"; return 1; }
|
||||||
|
lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
|
||||||
|
journalctl -u act_runner -n "$lines" --no-pager
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner_restart() {
|
||||||
|
local name="$1"; [ -z "$name" ] && { error "runner restart NAME required"; return 1; }
|
||||||
|
require_root
|
||||||
|
_runner_lxc_running "$name" || { error "LXC not running, start it first"; return 1; }
|
||||||
|
lxc-attach -n "$(_runner_lxc_name "$name")" -P "$LXC_PATH" -- \
|
||||||
|
systemctl restart act_runner
|
||||||
|
log "act_runner restarted in $(_runner_lxc_name "$name")"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_runner() {
|
||||||
|
local act="${1:-}"; shift || true
|
||||||
|
case "$act" in
|
||||||
|
token)
|
||||||
|
local sub="${1:-}"
|
||||||
|
case "$sub" in
|
||||||
|
gen|generate) cmd_runner_token_gen ;;
|
||||||
|
*) echo "Usage: giteactl runner token gen" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
add) cmd_runner_add "$@" ;;
|
||||||
|
remove|rm|delete) cmd_runner_remove "$@" ;;
|
||||||
|
list|ls) cmd_runner_list ;;
|
||||||
|
logs) cmd_runner_logs "$@" ;;
|
||||||
|
restart) cmd_runner_restart "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Runner commands (issue #190 — CI execution layer):
|
||||||
|
runner token gen Generate one-shot registration token
|
||||||
|
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||||
|
Bootstrap LXC, install gitea-runner, register, start
|
||||||
|
runner remove NAME [--keep-data] Stop + destroy LXC (Gitea entry stays)
|
||||||
|
runner list Local LXCs + registered runners (JSON)
|
||||||
|
runner logs NAME [--lines N] Tail in-LXC act_runner journal
|
||||||
|
runner restart NAME systemctl restart act_runner in the LXC
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Backup / Restore
|
# Backup / Restore
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -1093,6 +1364,14 @@ Repos & Mirrors (issue #176, parallel to mitmproxyctl route):
|
||||||
repo mirror sync OWNER/NAME
|
repo mirror sync OWNER/NAME
|
||||||
repo mirror list
|
repo mirror list
|
||||||
|
|
||||||
|
Runners (issue #190 — LXC-only act_runner, CI execution layer):
|
||||||
|
runner token gen
|
||||||
|
runner add NAME --labels L1,L2 [--arch arm64|amd64] [--memory 1G]
|
||||||
|
runner remove NAME [--keep-data]
|
||||||
|
runner list
|
||||||
|
runner logs NAME [--lines N]
|
||||||
|
runner restart NAME
|
||||||
|
|
||||||
Backup:
|
Backup:
|
||||||
backup [name] Create backup
|
backup [name] Create backup
|
||||||
restore <file> Restore from backup
|
restore <file> Restore from backup
|
||||||
|
|
@ -1129,6 +1408,7 @@ case "${1:-}" in
|
||||||
restart) shift; cmd_restart "$@" ;;
|
restart) shift; cmd_restart "$@" ;;
|
||||||
user) shift; cmd_user "$@" ;;
|
user) shift; cmd_user "$@" ;;
|
||||||
repo) shift; cmd_repo "$@" ;;
|
repo) shift; cmd_repo "$@" ;;
|
||||||
|
runner) shift; cmd_runner "$@" ;;
|
||||||
backup) shift; cmd_backup "$@" ;;
|
backup) shift; cmd_backup "$@" ;;
|
||||||
restore) shift; cmd_restore "$@" ;;
|
restore) shift; cmd_restore "$@" ;;
|
||||||
migrate) shift; cmd_migrate "$@" ;;
|
migrate) shift; cmd_migrate "$@" ;;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
secubox-metablogizer (1.1.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* metablogizerctl: forge tor noun verbs (issue #184) — the Emancipate
|
||||||
|
verb of the Punk Exposure Engine at the publishing layer.
|
||||||
|
Subcommands:
|
||||||
|
tor expose <site> Publish site via Tor hidden service
|
||||||
|
tor revoke <site> Stop publishing via Tor
|
||||||
|
tor list List Tor-exposed sites with onion addresses
|
||||||
|
tor status <site> Show stanza + onion + tor service state
|
||||||
|
When secubox-exposure is installed, delegates to it for consistency
|
||||||
|
with other exposure channels (DNS+SSL, Mesh). Otherwise falls back
|
||||||
|
to direct /etc/tor/secubox-metablogizer.d/ stanza management.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:23:12 +0200
|
||||||
|
|
||||||
secubox-metablogizer (1.1.0-1~bookworm1) bookworm; urgency=medium
|
secubox-metablogizer (1.1.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
* Add metablogizerctl three-fold commands: components, access (JSON output)
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,155 @@ site_unpublish() {
|
||||||
log "Site unpublished: $name"
|
log "Site unpublished: $name"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Tor — Emancipate verb (Punk Exposure Engine, issue #184)
|
||||||
|
#
|
||||||
|
# Per CLAUDE.md the Punk Exposure Engine has three verbs (Peek/Poke/Emancipate)
|
||||||
|
# and Tor is one of the three exposure channels. `metablogizerctl tor expose`
|
||||||
|
# is the Emancipate verb at the publishing layer for static sites.
|
||||||
|
#
|
||||||
|
# Implementation: write a per-site Tor HiddenService stanza, reload tor,
|
||||||
|
# read the generated .onion hostname back. If secubox-exposure is installed,
|
||||||
|
# delegate to it for consistency with other channels; otherwise fall back
|
||||||
|
# to direct torrc manipulation.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
TOR_DROPIN_DIR="${TOR_DROPIN_DIR:-/etc/tor/secubox-metablogizer.d}"
|
||||||
|
TOR_DATA_DIR="${TOR_DATA_DIR:-/var/lib/tor/secubox-metablogizer}"
|
||||||
|
|
||||||
|
_tor_drop_path() { echo "$TOR_DROPIN_DIR/${1}.conf"; }
|
||||||
|
_tor_data_path() { echo "$TOR_DATA_DIR/${1}"; }
|
||||||
|
|
||||||
|
_have_exposure() { command -v secubox-exposure >/dev/null 2>&1; }
|
||||||
|
_have_tor() { command -v tor >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
tor_expose() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor expose <site>"; return 1; }
|
||||||
|
|
||||||
|
local site_dir="$SITES_ROOT/$name"
|
||||||
|
[ -d "$site_dir" ] || { error "site not found: $name (run: metablogizerctl site create $name first)"; return 1; }
|
||||||
|
|
||||||
|
# Prefer secubox-exposure when available so all 3 exposure channels share
|
||||||
|
# the same orchestration (Tor / DNS+SSL / Mesh) and Peek shows it.
|
||||||
|
if _have_exposure; then
|
||||||
|
log "delegating to secubox-exposure emancipate (tor channel)"
|
||||||
|
# The static site is served via nginx on port 80 (per site_publish);
|
||||||
|
# secubox-exposure handles the HiddenService wiring and revocation.
|
||||||
|
secubox-exposure emancipate "metablogizer-${name}" 80 --tor
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: write a tor drop-in directly.
|
||||||
|
_have_tor || { error "tor not installed and secubox-exposure unavailable"; return 1; }
|
||||||
|
mkdir -p "$TOR_DROPIN_DIR"
|
||||||
|
mkdir -p "$TOR_DATA_DIR"
|
||||||
|
local data; data=$(_tor_data_path "$name")
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
log "writing Tor HiddenService stanza for $name"
|
||||||
|
cat > "$drop" <<EOF
|
||||||
|
# metablogizer site: $name (#184 — Emancipate via Tor)
|
||||||
|
HiddenServiceDir $data
|
||||||
|
HiddenServicePort 80 127.0.0.1:80
|
||||||
|
EOF
|
||||||
|
chown -R debian-tor:debian-tor "$TOR_DATA_DIR" 2>/dev/null || true
|
||||||
|
chmod 700 "$data" 2>/dev/null || true
|
||||||
|
log "reloading tor"
|
||||||
|
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||||
|
# Wait briefly for tor to publish the hostname file
|
||||||
|
local i=0
|
||||||
|
while [ $i -lt 10 ] && [ ! -f "$data/hostname" ]; do sleep 1; i=$((i+1)); done
|
||||||
|
if [ -f "$data/hostname" ]; then
|
||||||
|
local onion; onion=$(cat "$data/hostname")
|
||||||
|
log "site emancipated via Tor: $onion"
|
||||||
|
# Persist the address back into site.json for future Peek calls
|
||||||
|
if [ -f "$site_dir/site.json" ] && command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
p='$site_dir/site.json'
|
||||||
|
d=json.load(open(p))
|
||||||
|
d.setdefault('exposure',{})['tor']='$onion'
|
||||||
|
json.dump(d, open(p,'w'), indent=2)
|
||||||
|
" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "tor reload OK but hostname not yet written; check 'metablogizerctl tor status $name'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_revoke() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor revoke <site>"; return 1; }
|
||||||
|
if _have_exposure; then
|
||||||
|
log "delegating to secubox-exposure revoke"
|
||||||
|
secubox-exposure revoke "metablogizer-${name}" --tor
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
[ -f "$drop" ] || { warn "no Tor stanza for $name"; return 0; }
|
||||||
|
rm -f "$drop"
|
||||||
|
systemctl reload tor 2>/dev/null || systemctl restart tor
|
||||||
|
log "tor stanza removed for $name (data dir kept under $TOR_DATA_DIR/$name — delete manually if desired)"
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_list() {
|
||||||
|
if _have_exposure; then
|
||||||
|
log "(delegate) secubox-exposure list --tor"
|
||||||
|
secubox-exposure list --tor 2>/dev/null && return
|
||||||
|
fi
|
||||||
|
if [ ! -d "$TOR_DROPIN_DIR" ]; then
|
||||||
|
echo "(no Tor-exposed sites)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local any=0
|
||||||
|
for d in "$TOR_DROPIN_DIR"/*.conf; do
|
||||||
|
[ -f "$d" ] || continue
|
||||||
|
any=1
|
||||||
|
local n; n=$(basename "$d" .conf)
|
||||||
|
local h="$TOR_DATA_DIR/$n/hostname"
|
||||||
|
if [ -f "$h" ]; then
|
||||||
|
printf " %-30s -> %s\n" "$n" "$(cat "$h")"
|
||||||
|
else
|
||||||
|
printf " %-30s -> (publishing...)\n" "$n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ $any = 0 ] && echo "(no Tor-exposed sites)"
|
||||||
|
}
|
||||||
|
|
||||||
|
tor_status() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: metablogizerctl tor status <site>"; return 1; }
|
||||||
|
local data; data=$(_tor_data_path "$name")
|
||||||
|
local drop; drop=$(_tor_drop_path "$name")
|
||||||
|
echo "site: $name"
|
||||||
|
echo "stanza present: $([ -f "$drop" ] && echo yes || echo no)"
|
||||||
|
if [ -f "$data/hostname" ]; then
|
||||||
|
echo "onion: $(cat "$data/hostname")"
|
||||||
|
else
|
||||||
|
echo "onion: (not yet published)"
|
||||||
|
fi
|
||||||
|
systemctl is-active tor >/dev/null 2>&1 && echo "tor service: active" || echo "tor service: inactive"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_tor() {
|
||||||
|
local action="${1:-}"; shift || true
|
||||||
|
case "$action" in
|
||||||
|
expose) tor_expose "$@" ;;
|
||||||
|
revoke|remove) tor_revoke "$@" ;;
|
||||||
|
list|ls) tor_list ;;
|
||||||
|
status) tor_status "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Tor commands (Punk Exposure / Emancipate verb, issue #184):
|
||||||
|
tor expose <site> - publish site via Tor hidden service
|
||||||
|
tor revoke <site> - stop publishing via Tor
|
||||||
|
tor list - list Tor-exposed sites + their onion addresses
|
||||||
|
tor status <site> - show stanza presence + onion + tor service state
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
site_list() {
|
site_list() {
|
||||||
echo "MetaBlogizer Sites:"
|
echo "MetaBlogizer Sites:"
|
||||||
echo "==================="
|
echo "==================="
|
||||||
|
|
@ -387,6 +536,12 @@ Sites:
|
||||||
site unpublish <name> Unpublish site
|
site unpublish <name> Unpublish site
|
||||||
site list List all sites
|
site list List all sites
|
||||||
|
|
||||||
|
Tor (Punk Exposure / Emancipate, issue #184):
|
||||||
|
tor expose <site> Publish site via Tor hidden service
|
||||||
|
tor revoke <site> Stop publishing via Tor
|
||||||
|
tor list List Tor-exposed sites + onions
|
||||||
|
tor status <site> Stanza + onion + tor service state
|
||||||
|
|
||||||
Service:
|
Service:
|
||||||
migrate [host] Migrate from OpenWrt
|
migrate [host] Migrate from OpenWrt
|
||||||
|
|
||||||
|
|
@ -394,6 +549,7 @@ Examples:
|
||||||
metablogizerctl components # JSON components
|
metablogizerctl components # JSON components
|
||||||
metablogizerctl site create myblog blog.example.com
|
metablogizerctl site create myblog blog.example.com
|
||||||
metablogizerctl site publish myblog
|
metablogizerctl site publish myblog
|
||||||
|
metablogizerctl tor expose myblog # Emancipate via Tor
|
||||||
metablogizerctl migrate 192.168.255.1
|
metablogizerctl migrate 192.168.255.1
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -422,6 +578,8 @@ case "${1:-}" in
|
||||||
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
*) echo "Usage: metablogizerctl site create|delete|publish|unpublish|list" ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
# Tor (Emancipate, issue #184)
|
||||||
|
tor) shift; cmd_tor "$@" ;;
|
||||||
migrate) shift; cmd_migrate "$@" ;;
|
migrate) shift; cmd_migrate "$@" ;;
|
||||||
help|--help|-h|'') show_help ;;
|
help|--help|-h|'') show_help ;;
|
||||||
*) error "Unknown: $1"; exit 1 ;;
|
*) error "Unknown: $1"; exit 1 ;;
|
||||||
|
|
|
||||||
49
packages/secubox-metrics/bin/secubox-geoipupdate-fetch
Executable file
49
packages/secubox-metrics/bin/secubox-geoipupdate-fetch
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# secubox-geoipupdate-fetch — refresh /var/lib/GeoIP/GeoLite2-ASN.mmdb
|
||||||
|
#
|
||||||
|
# Two paths (#194):
|
||||||
|
# 1. If /etc/secubox/secrets/maxmind.conf exists AND `geoipupdate` is
|
||||||
|
# installed, use the MaxMind GeoLite2 download (operator has a key).
|
||||||
|
# 2. Otherwise fall back to DB-IP free ASN lite, no signup required.
|
||||||
|
# Format is MaxMind-compatible — the visitor_origin aggregator reads
|
||||||
|
# it with `maxminddb.open_database` transparently.
|
||||||
|
#
|
||||||
|
# Invoked by secubox-geoipupdate.service (weekly timer).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MMDB_DEST="/var/lib/GeoIP/GeoLite2-ASN.mmdb"
|
||||||
|
MAXMIND_CONF="/etc/secubox/secrets/maxmind.conf"
|
||||||
|
DBIP_URL_BASE="https://download.db-ip.com/free"
|
||||||
|
|
||||||
|
log() { echo "[geoipupdate-fetch] $*"; }
|
||||||
|
err() { echo "[geoipupdate-fetch] ERROR: $*" >&2; }
|
||||||
|
|
||||||
|
install -d -m 755 /var/lib/GeoIP
|
||||||
|
|
||||||
|
if [ -f "$MAXMIND_CONF" ] && command -v geoipupdate >/dev/null 2>&1; then
|
||||||
|
log "MaxMind license + geoipupdate present — using MaxMind path"
|
||||||
|
exec geoipupdate -f "$MAXMIND_CONF" -d /var/lib/GeoIP
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "no MaxMind license (or geoipupdate missing) — falling back to DB-IP free"
|
||||||
|
MONTH=$(date +%Y-%m)
|
||||||
|
URL="${DBIP_URL_BASE}/dbip-asn-lite-${MONTH}.mmdb.gz"
|
||||||
|
TMP=$(mktemp)
|
||||||
|
TMP_GZ="${TMP}.gz"
|
||||||
|
trap 'rm -f "$TMP" "$TMP_GZ"' EXIT
|
||||||
|
|
||||||
|
log "GET $URL"
|
||||||
|
if ! curl -fsSL -o "$TMP_GZ" "$URL"; then
|
||||||
|
err "DB-IP download failed; trying previous month as fallback"
|
||||||
|
LAST_MONTH=$(date -d "${MONTH}-01 -1 day" +%Y-%m 2>/dev/null || date -v-1m +%Y-%m)
|
||||||
|
URL="${DBIP_URL_BASE}/dbip-asn-lite-${LAST_MONTH}.mmdb.gz"
|
||||||
|
log "GET $URL"
|
||||||
|
curl -fsSL -o "$TMP_GZ" "$URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
gunzip -f "$TMP_GZ" # writes $TMP
|
||||||
|
install -m 644 -o secubox -g secubox "$TMP" "$MMDB_DEST"
|
||||||
|
log "installed $MMDB_DEST ($(stat -c %s "$MMDB_DEST") bytes)"
|
||||||
|
|
@ -1,3 +1,24 @@
|
||||||
|
secubox-metrics (1.0.3-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* VisitorOrigin: add AmbientCapabilities=CAP_NET_ADMIN to the systemd
|
||||||
|
unit (#194, latent since #92). Without this the aggregator runs as
|
||||||
|
secubox user, `nft -j list set inet secubox_metrics seen_src` returns
|
||||||
|
EPERM, _read_nft_set silently returns [] and entries stays empty
|
||||||
|
forever. Caps coexist with NoNewPrivileges=true.
|
||||||
|
|
||||||
|
* secubox-geoipupdate: drop the ConditionPathExists=/etc/secubox/
|
||||||
|
secrets/maxmind.conf gate. New helper `secubox-geoipupdate-fetch`
|
||||||
|
tries MaxMind first if a license is present, else falls back to
|
||||||
|
DB-IP free ASN lite (https://download.db-ip.com/free/dbip-asn-lite-
|
||||||
|
YYYY-MM.mmdb.gz). DB-IP releases mmdb in MaxMind-compatible format
|
||||||
|
so the visitor_origin aggregator reads it transparently.
|
||||||
|
|
||||||
|
* Operators get VisitorOrigin out of the box now (no MaxMind account
|
||||||
|
required); those with a license keep using MaxMind via the same
|
||||||
|
helper.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 18 May 2026 06:16:28 +0200
|
||||||
|
|
||||||
secubox-metrics (1.0.2-1~bookworm1) bookworm; urgency=medium
|
secubox-metrics (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add Cookie Audit (RGPD / ePrivacy) endpoints: POST /api/v1/cookie-audit/ingest,
|
* Add Cookie Audit (RGPD / ePrivacy) endpoints: POST /api/v1/cookie-audit/ingest,
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,7 @@ override_dh_install:
|
||||||
debian/secubox-metrics/lib/systemd/system/secubox-geoipupdate.service
|
debian/secubox-metrics/lib/systemd/system/secubox-geoipupdate.service
|
||||||
install -D -m 0644 systemd/secubox-geoipupdate.timer \
|
install -D -m 0644 systemd/secubox-geoipupdate.timer \
|
||||||
debian/secubox-metrics/lib/systemd/system/secubox-geoipupdate.timer
|
debian/secubox-metrics/lib/systemd/system/secubox-geoipupdate.timer
|
||||||
|
# Helper fetcher (#194): tries MaxMind first if license present,
|
||||||
|
# else falls back to DB-IP free ASN lite (no signup required).
|
||||||
|
install -D -m 0755 bin/secubox-geoipupdate-fetch \
|
||||||
|
debian/secubox-metrics/usr/bin/secubox-geoipupdate-fetch
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,13 @@ RestartSec=5
|
||||||
# ProtectSystem=full
|
# ProtectSystem=full
|
||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
ReadWritePaths=/run/secubox /var/cache/secubox
|
ReadWritePaths=/run/secubox /var/cache/secubox
|
||||||
|
# VisitorOrigin reads the nft `seen_src` set via `nft -j list set ...` which
|
||||||
|
# requires CAP_NET_ADMIN. Without it, _read_nft_set silently returns [] and
|
||||||
|
# the aggregator emits empty entries forever (#194 fix, latent since #92).
|
||||||
|
# Coexists with NoNewPrivileges=true because systemd sets the ambient set
|
||||||
|
# before the User= drop.
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SecuBox — refresh GeoLite2 ASN database
|
Description=SecuBox — refresh GeoLite2-ASN database (MaxMind or DB-IP fallback)
|
||||||
ConditionPathExists=/etc/secubox/secrets/maxmind.conf
|
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
User=secubox
|
# Fetcher must run as root because /var/lib/GeoIP is owned by root and the
|
||||||
Group=secubox
|
# DB-IP fallback path uses `install -o secubox` to land the mmdb under the
|
||||||
ExecStart=/usr/bin/geoipupdate -f /etc/secubox/secrets/maxmind.conf -d /var/lib/GeoIP
|
# correct ownership. The helper does not exec anything as root that takes
|
||||||
NoNewPrivileges=true
|
# untrusted input.
|
||||||
|
User=root
|
||||||
|
ExecStart=/usr/bin/secubox-geoipupdate-fetch
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ReadWritePaths=/var/lib/GeoIP
|
ReadWritePaths=/var/lib/GeoIP
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,23 @@
|
||||||
|
secubox-portal (2.2.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* portal: regenerate /portal/index.html as a public-facing operational
|
||||||
|
dashboard (issue #192). Consumes unauthenticated metrics endpoints
|
||||||
|
(cert-status, visitor-origin, live-hosts, health/summary, cookie-
|
||||||
|
audit/summary, crowdsec/decisions) and renders:
|
||||||
|
- Navbar (portal/soc/metablogs/publish/cookies/repo)
|
||||||
|
- Hero (overall health score + uptime/SSL/vhosts/certs/bans)
|
||||||
|
- Services LED grid (waf/crowdsec/haproxy/nginx/system)
|
||||||
|
- System metrics (CPU/RAM/Disk/Load/LXC) with progress bars
|
||||||
|
- Certificates summary (total/valid/expiring/critical/expired)
|
||||||
|
- Cookie Audit · RGPD breakdown
|
||||||
|
- Attacks & Bans tiles (active bans / WAF % / today alerts)
|
||||||
|
- Top vhosts (last 60 min traffic)
|
||||||
|
- Top visitor ASN (geographic attribution)
|
||||||
|
Auto-refresh 30s, graceful degradation on endpoint failure.
|
||||||
|
Style: P31 phosphor green CRT (preserved from previous version).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 18 May 2026 05:54:39 +0200
|
||||||
|
|
||||||
secubox-portal (2.1.0-1~bookworm1) bookworm; urgency=medium
|
secubox-portal (2.1.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add device-specific theming based on detected board
|
* Add device-specific theming based on detected board
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SecuBox - Portal</title>
|
<title>SecuBox · Portal</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||||
|
|
@ -20,16 +21,13 @@
|
||||||
--tube-pale: #c8e6c9;
|
--tube-pale: #c8e6c9;
|
||||||
--tube-soft: #a5d6a7;
|
--tube-soft: #a5d6a7;
|
||||||
--tube-dark: #1b1b1f;
|
--tube-dark: #1b1b1f;
|
||||||
--text-dim: var(--p31-dim);
|
|
||||||
--primary: var(--p31-peak);
|
--primary: var(--p31-peak);
|
||||||
--cyan: #00d4ff;
|
--cyan: #00d4ff;
|
||||||
--green: var(--p31-peak);
|
|
||||||
--red: #ff4466;
|
--red: #ff4466;
|
||||||
--yellow: var(--p31-decay);
|
--yellow: var(--p31-decay);
|
||||||
--purple: #a371f7;
|
|
||||||
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
||||||
--bloom-soft: 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
|
||||||
--bloom-amber: 0 0 3px var(--p31-decay), 0 0 10px rgba(255,179,71,0.4);
|
--bloom-amber: 0 0 3px var(--p31-decay), 0 0 10px rgba(255,179,71,0.4);
|
||||||
|
--bloom-red: 0 0 3px var(--red), 0 0 10px rgba(255,68,102,0.4);
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
body {
|
||||||
|
|
@ -38,471 +36,462 @@
|
||||||
background-image: radial-gradient(ellipse at 50% 40%, rgba(51,255,102,0.025) 0%, transparent 70%);
|
background-image: radial-gradient(ellipse at 50% 40%, rgba(51,255,102,0.025) 0%, transparent 70%);
|
||||||
color: var(--tube-dark);
|
color: var(--tube-dark);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 2rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
body::before {
|
body::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: fixed;
|
position: fixed; inset: 0;
|
||||||
inset: 0;
|
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(rgba(0,221,68,0.02) 1px, transparent 1px),
|
linear-gradient(rgba(0,221,68,0.02) 1px, transparent 1px),
|
||||||
linear-gradient(90deg, rgba(0,221,68,0.02) 1px, transparent 1px);
|
linear-gradient(90deg, rgba(0,221,68,0.02) 1px, transparent 1px);
|
||||||
background-size: 50px 50px;
|
background-size: 50px 50px;
|
||||||
pointer-events: none;
|
pointer-events: none; z-index: -1;
|
||||||
z-index: -1;
|
|
||||||
}
|
}
|
||||||
.container { max-width: 1400px; margin: 0 auto; }
|
a { color: var(--p31-mid); text-decoration: none; }
|
||||||
|
a:hover { color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
||||||
|
|
||||||
/* Header */
|
/* Navbar */
|
||||||
.header { text-align: center; margin-bottom: 3rem; }
|
.navbar {
|
||||||
.logo-icon {
|
background: var(--tube-dark);
|
||||||
display: inline-flex;
|
color: var(--p31-peak);
|
||||||
align-items: center;
|
padding: 0.75rem 2rem;
|
||||||
justify-content: center;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
width: 100px;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
height: 100px;
|
position: sticky; top: 0; z-index: 50;
|
||||||
background: linear-gradient(135deg, var(--p31-peak), var(--p31-mid));
|
border-bottom: 1px solid var(--p31-mid);
|
||||||
border-radius: 24px;
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
box-shadow: 0 8px 32px rgba(0,221,68,0.3);
|
|
||||||
}
|
}
|
||||||
.logo {
|
.navbar .brand {
|
||||||
font-size: 2.5rem;
|
font-weight: bold; font-size: 1.1rem; letter-spacing: 1px;
|
||||||
font-weight: 700;
|
|
||||||
color: var(--p31-hot);
|
|
||||||
text-shadow: var(--bloom-text);
|
text-shadow: var(--bloom-text);
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
.tagline { color: var(--text-dim); font-size: 1.1rem; margin-top: 0.5rem; }
|
.navbar .brand a { color: var(--p31-peak); }
|
||||||
|
.navbar .nav-links { display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
||||||
|
.navbar .nav-links a {
|
||||||
|
color: var(--p31-mid); font-size: 0.9rem;
|
||||||
|
padding: 0.25rem 0.5rem; border-radius: 3px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.navbar .nav-links a:hover { color: var(--p31-peak); background: rgba(0,221,68,0.08); text-shadow: var(--bloom-text); }
|
||||||
|
.navbar .nav-meta { font-size: 0.75rem; color: var(--p31-dim); }
|
||||||
|
|
||||||
/* Quick Actions */
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
/* Hero */
|
||||||
justify-content: center;
|
.hero {
|
||||||
gap: 1rem;
|
margin-bottom: 2rem;
|
||||||
margin-top: 1.5rem;
|
padding: 1.5rem 2rem;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.quick-btn {
|
|
||||||
padding: 0.6rem 1.2rem;
|
|
||||||
background: var(--tube-pale);
|
background: var(--tube-pale);
|
||||||
border: 1px solid var(--tube-soft);
|
border: 1px solid var(--p31-mid);
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--p31-mid);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.quick-btn:hover {
|
|
||||||
border-color: var(--p31-peak);
|
|
||||||
color: var(--p31-peak);
|
|
||||||
text-shadow: var(--bloom-soft);
|
|
||||||
}
|
|
||||||
.quick-btn.primary {
|
|
||||||
background: rgba(0,221,68,0.1);
|
|
||||||
border-color: var(--p31-peak);
|
|
||||||
color: var(--p31-peak);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats Bar */
|
|
||||||
.stats-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.stat {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
background: var(--tube-pale);
|
|
||||||
border: 1px solid var(--tube-soft);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.stat-value {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--p31-peak);
|
|
||||||
text-shadow: 0 0 10px var(--p31-peak);
|
|
||||||
}
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sections */
|
|
||||||
.section { margin-bottom: 2.5rem; }
|
|
||||||
.section-title {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.15em;
|
|
||||||
color: var(--p31-decay);
|
|
||||||
text-shadow: var(--bloom-amber);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
border-left: 3px solid var(--p31-decay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Service Grid */
|
|
||||||
.services-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.service-card {
|
|
||||||
background: var(--tube-pale);
|
|
||||||
border: 1px solid var(--tube-soft);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.25rem;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--tube-dark);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.service-card:hover {
|
|
||||||
border-color: var(--p31-dim);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
.service-card .icon {
|
|
||||||
font-size: 2rem;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.service-card .name { font-weight: 600; font-size: 0.95rem; }
|
|
||||||
.service-card .desc { font-size: 0.7rem; color: var(--text-dim); }
|
|
||||||
.service-card .status {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: auto;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
.status.online { background: rgba(0,221,68,0.15); color: var(--green); }
|
|
||||||
.status.offline { background: rgba(255,68,102,0.15); color: var(--red); }
|
|
||||||
.status.external { background: rgba(0,212,255,0.15); color: var(--cyan); }
|
|
||||||
|
|
||||||
/* Icon backgrounds */
|
|
||||||
.icon.security { background: rgba(255,68,102,0.1); }
|
|
||||||
.icon.network { background: rgba(0,212,255,0.1); }
|
|
||||||
.icon.monitoring { background: rgba(0,221,68,0.1); }
|
|
||||||
.icon.apps { background: rgba(163,113,247,0.1); }
|
|
||||||
.icon.comm { background: rgba(255,179,71,0.1); }
|
|
||||||
|
|
||||||
/* Published Sites */
|
|
||||||
.published-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.published-card {
|
|
||||||
background: var(--tube-pale);
|
|
||||||
border: 1px solid var(--tube-soft);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.25rem;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--tube-dark);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.published-card:hover {
|
|
||||||
border-color: var(--p31-dim);
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.published-card .title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.published-card .domain {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--cyan);
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
.published-card .meta {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.published-card .badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.65rem;
|
box-shadow: 0 0 12px rgba(0,221,68,0.15);
|
||||||
text-transform: uppercase;
|
display: grid;
|
||||||
letter-spacing: 0.05em;
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
.badge.ssl { background: rgba(0,221,68,0.15); color: var(--green); }
|
.hero-score {
|
||||||
.badge.static { background: rgba(163,113,247,0.15); color: var(--purple); }
|
display: flex; flex-direction: column; align-items: center;
|
||||||
.badge.proxy { background: rgba(0,212,255,0.15); color: var(--cyan); }
|
min-width: 140px;
|
||||||
|
|
||||||
/* Empty State */
|
|
||||||
.empty {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
}
|
||||||
|
.hero-score .num {
|
||||||
|
font-size: 3.5rem; font-weight: bold; line-height: 1;
|
||||||
|
color: var(--p31-peak); text-shadow: var(--bloom-text);
|
||||||
|
}
|
||||||
|
.hero-score .label { font-size: 0.8rem; color: var(--p31-dim); margin-top: 0.25rem; letter-spacing: 1px; }
|
||||||
|
.hero-score.warn .num { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
||||||
|
.hero-score.crit .num { color: var(--red); text-shadow: var(--bloom-red); }
|
||||||
|
.hero-info { font-size: 0.95rem; line-height: 1.6; }
|
||||||
|
.hero-info .kv { display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
||||||
|
.hero-info .k { color: var(--p31-dim); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
.hero-info .v { color: var(--tube-dark); font-weight: bold; }
|
||||||
|
.hero-refresh { font-size: 0.75rem; color: var(--p31-dim); text-align: right; }
|
||||||
|
|
||||||
|
/* Section grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--tube-light);
|
||||||
|
border: 1px solid var(--p31-mid);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 8px rgba(0,221,68,0.08);
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px;
|
||||||
|
color: var(--p31-mid); margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px dashed var(--p31-ghost);
|
||||||
|
text-shadow: var(--bloom-text);
|
||||||
|
}
|
||||||
|
.card .row { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 0.9rem; }
|
||||||
|
.card .row .k { color: var(--p31-dim); }
|
||||||
|
.card .row .v { color: var(--tube-dark); font-weight: bold; }
|
||||||
|
.card .row .v.ok { color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
||||||
|
.card .row .v.warn { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
||||||
|
.card .row .v.crit { color: var(--red); text-shadow: var(--bloom-red); }
|
||||||
|
.empty { color: var(--p31-dim); font-style: italic; font-size: 0.85rem; padding: 0.5rem 0; }
|
||||||
|
|
||||||
|
/* Bar */
|
||||||
|
.bar {
|
||||||
|
height: 4px; background: var(--p31-ghost); border-radius: 2px;
|
||||||
|
overflow: hidden; margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
.bar > span { display: block; height: 100%; background: var(--p31-peak); }
|
||||||
|
.bar.warn > span { background: var(--yellow); }
|
||||||
|
.bar.crit > span { background: var(--red); }
|
||||||
|
|
||||||
|
/* Modules LED grid */
|
||||||
|
.modules { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.5rem; }
|
||||||
|
.module {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
gap: 0.25rem; padding: 0.5rem; border-radius: 4px;
|
||||||
|
background: var(--tube-pale); border: 1px solid var(--p31-ghost);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
.module .led {
|
||||||
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
|
background: var(--p31-dim);
|
||||||
|
}
|
||||||
|
.module.ok .led { background: var(--p31-peak); box-shadow: 0 0 8px var(--p31-peak); }
|
||||||
|
.module.warn .led { background: var(--yellow); box-shadow: 0 0 8px var(--yellow); }
|
||||||
|
.module.err .led { background: var(--red); box-shadow: 0 0 8px var(--red); }
|
||||||
|
.module .name { font-size: 0.65rem; text-transform: uppercase; color: var(--p31-dim); }
|
||||||
|
|
||||||
|
/* Tables — vhosts / ASNs */
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||||
|
th, td { text-align: left; padding: 0.25rem 0.5rem; }
|
||||||
|
th { font-size: 0.7rem; color: var(--p31-dim); text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px dashed var(--p31-ghost); }
|
||||||
|
td.num { text-align: right; color: var(--cyan); font-variant-numeric: tabular-nums; }
|
||||||
|
tr:hover td { background: rgba(0,221,68,0.04); }
|
||||||
|
|
||||||
|
/* Big number tiles */
|
||||||
|
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; }
|
||||||
|
.tile {
|
||||||
|
background: var(--tube-pale); border: 1px solid var(--p31-ghost);
|
||||||
|
border-radius: 4px; padding: 0.75rem; text-align: center;
|
||||||
|
}
|
||||||
|
.tile .v { font-size: 1.75rem; font-weight: bold; color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
||||||
|
.tile .v.warn { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
||||||
|
.tile .v.crit { color: var(--red); text-shadow: var(--bloom-red); }
|
||||||
|
.tile .l { font-size: 0.7rem; text-transform: uppercase; color: var(--p31-dim); letter-spacing: 1px; }
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
.footer {
|
||||||
text-align: center;
|
margin-top: 3rem; padding-top: 1.5rem;
|
||||||
margin-top: 3rem;
|
border-top: 1px dashed var(--p31-ghost);
|
||||||
padding-top: 2rem;
|
text-align: center; font-size: 0.8rem; color: var(--p31-dim);
|
||||||
border-top: 1px solid var(--tube-soft);
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
}
|
||||||
.footer a { color: var(--p31-mid); text-decoration: none; }
|
.footer a { color: var(--p31-mid); }
|
||||||
.footer a:hover { color: var(--p31-peak); }
|
|
||||||
|
|
||||||
/* Health Banner Space */
|
|
||||||
body { padding-top: 3rem; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body { padding: 1rem; padding-top: 3rem; }
|
.hero { grid-template-columns: 1fr; }
|
||||||
.stats-bar { gap: 1rem; }
|
.modules { grid-template-columns: repeat(3, 1fr); }
|
||||||
.stat { padding: 0.75rem 1rem; }
|
.navbar { padding: 0.75rem 1rem; flex-direction: column; gap: 0.5rem; }
|
||||||
.stat-value { font-size: 1.5rem; }
|
.navbar .nav-links { font-size: 0.8rem; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="crt-light">
|
<body>
|
||||||
<div class="container">
|
|
||||||
<header class="header">
|
|
||||||
<div class="logo-icon">🔐</div>
|
|
||||||
<h1 class="logo" id="logo-text">SecuBox</h1>
|
|
||||||
<p class="tagline" id="tagline">Secure Network Appliance — Public Portal</p>
|
|
||||||
<div class="quick-actions">
|
|
||||||
<a href="/" class="quick-btn">📊 Dashboard</a>
|
|
||||||
<a href="/c3box/" class="quick-btn">🌐 Services</a>
|
|
||||||
<a href="/soc/" class="quick-btn">🛡️ SOC</a>
|
|
||||||
<a href="/login.html" class="quick-btn primary" id="auth-btn">🔑 Login</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="stats-bar">
|
<nav class="navbar">
|
||||||
<div class="stat">
|
<div class="brand"><a href="/portal/">🛡️ SecuBox</a></div>
|
||||||
<div class="stat-value" id="stat-services">-</div>
|
<div class="nav-links">
|
||||||
<div class="stat-label">Services Online</div>
|
<a href="/portal/">PORTAL</a>
|
||||||
</div>
|
<a href="/soc/">SOC</a>
|
||||||
<div class="stat">
|
<a href="/metablogizer/">METABLOGS</a>
|
||||||
<div class="stat-value" id="stat-sites">-</div>
|
<a href="/publish/">PUBLISH</a>
|
||||||
<div class="stat-label">Published Sites</div>
|
<a href="/cookies/">COOKIES</a>
|
||||||
</div>
|
<a href="https://github.com/CyberMind-FR/secubox-deb" rel="noopener">REPO</a>
|
||||||
<div class="stat">
|
</div>
|
||||||
<div class="stat-value" id="stat-health">-</div>
|
<div class="nav-meta" id="nav-hostname">—</div>
|
||||||
<div class="stat-label">Health Score</div>
|
</nav>
|
||||||
</div>
|
|
||||||
<div class="stat">
|
<div class="container">
|
||||||
<div class="stat-value" id="stat-threats">-</div>
|
|
||||||
<div class="stat-label">Threats Blocked</div>
|
<!-- Hero -->
|
||||||
</div>
|
<section class="hero">
|
||||||
|
<div class="hero-score" id="hero-score">
|
||||||
|
<div class="num" id="hero-score-num">—</div>
|
||||||
|
<div class="label">HEALTH</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-info">
|
||||||
|
<div class="kv">
|
||||||
|
<div><div class="k">Hostname</div><div class="v" id="hero-hostname">—</div></div>
|
||||||
|
<div><div class="k">Uptime</div><div class="v" id="hero-uptime">—</div></div>
|
||||||
|
<div><div class="k">SSL</div><div class="v" id="hero-ssl">—</div></div>
|
||||||
|
<div><div class="k">VHosts</div><div class="v" id="hero-vhosts">—</div></div>
|
||||||
|
<div><div class="k">Certificates</div><div class="v" id="hero-certs">—</div></div>
|
||||||
|
<div><div class="k">Bans active</div><div class="v" id="hero-bans">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-refresh">
|
||||||
|
<div>last refresh</div>
|
||||||
|
<div id="hero-refresh">—</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<!-- Module status LEDs -->
|
||||||
<h2 class="section-title">🛡️ Security Services</h2>
|
<section class="card" style="margin-bottom: 1.5rem;">
|
||||||
<div class="services-grid" id="security-services"></div>
|
<h3>SERVICES</h3>
|
||||||
</section>
|
<div class="modules" id="modules-grid">
|
||||||
|
<div class="empty" style="grid-column: 1/-1;">Loading…</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<div class="grid">
|
||||||
<h2 class="section-title">🌐 Network Services</h2>
|
|
||||||
<div class="services-grid" id="network-services"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
<!-- System -->
|
||||||
<h2 class="section-title">📦 Applications</h2>
|
<div class="card">
|
||||||
<div class="services-grid" id="apps-services"></div>
|
<h3>System</h3>
|
||||||
</section>
|
<div id="sys-rows"><div class="empty">Loading…</div></div>
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h2 class="section-title">🚀 Published Sites</h2>
|
|
||||||
<div class="published-grid" id="published-sites">
|
|
||||||
<div class="empty">Loading published sites...</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h2 class="section-title">📡 External Services</h2>
|
|
||||||
<div class="services-grid" id="external-services"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>SecuBox © 2024-2026 <a href="https://cybermind.fr">CyberMind</a></p>
|
|
||||||
<p style="margin-top: 0.5rem;">Public Portal • <span id="hostname">-</span></p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Certs -->
|
||||||
const services = {
|
<div class="card">
|
||||||
security: [
|
<h3>Certificates</h3>
|
||||||
{ name: 'CrowdSec', icon: '🛡️', desc: 'IDS/IPS Protection', url: '/crowdsec/', iconClass: 'security' },
|
<div id="cert-rows"><div class="empty">Loading…</div></div>
|
||||||
{ name: 'WAF', icon: '🔥', desc: 'Web Application Firewall', url: '/waf/', iconClass: 'security' },
|
</div>
|
||||||
{ name: 'WireGuard', icon: '🔒', desc: 'VPN Server', url: '/wireguard/', iconClass: 'security' },
|
|
||||||
{ name: 'NAC', icon: '👁️', desc: 'Network Access Control', url: '/nac/', iconClass: 'security' },
|
<!-- Cookie audit / RGPD -->
|
||||||
],
|
<div class="card">
|
||||||
network: [
|
<h3>Cookie Audit · RGPD</h3>
|
||||||
{ name: 'HAProxy', icon: '⚡', desc: 'Load Balancer', url: '/haproxy/', iconClass: 'network' },
|
<div id="cookie-rows"><div class="empty">Loading…</div></div>
|
||||||
{ name: 'VHosts', icon: '🌐', desc: 'Virtual Hosts', url: '/vhost/', iconClass: 'network' },
|
</div>
|
||||||
{ name: 'DNS', icon: '🌍', desc: 'DNS Server', url: '/dns/', iconClass: 'network' },
|
|
||||||
{ name: 'QoS', icon: '📶', desc: 'Bandwidth Manager', url: '/qos/', iconClass: 'network' },
|
<!-- Bans (crowdsec decisions count + WAF %) -->
|
||||||
],
|
<div class="card">
|
||||||
apps: [
|
<h3>Attacks & Bans</h3>
|
||||||
{ name: 'Publish', icon: '🚀', desc: 'Publishing Hub', url: '/publish/', iconClass: 'apps' },
|
<div class="tiles" id="bans-tiles">
|
||||||
{ name: 'MetaBlogizer', icon: '📝', desc: 'Static Sites', url: '/metablogizer/', iconClass: 'apps' },
|
<div class="tile"><div class="v" id="t-bans">—</div><div class="l">Active bans</div></div>
|
||||||
{ name: 'Droplet', icon: '📁', desc: 'File Publisher', url: '/droplet/', iconClass: 'apps' },
|
<div class="tile"><div class="v" id="t-waf">—</div><div class="l">WAF blocked %</div></div>
|
||||||
{ name: 'Streamlit', icon: '📊', desc: 'Python Apps', url: '/streamlit/', iconClass: 'apps' },
|
<div class="tile"><div class="v" id="t-alerts">—</div><div class="l">Today alerts</div></div>
|
||||||
],
|
</div>
|
||||||
external: [
|
</div>
|
||||||
{ name: 'Netdata', icon: '📈', desc: 'Real-time Monitoring', url: '/netdata/', iconClass: 'monitoring', external: true },
|
|
||||||
{ name: 'CDN', icon: '🌐', desc: 'Cache Server', url: '/cdn/', iconClass: 'network' },
|
</div>
|
||||||
{ name: 'MediaFlow', icon: '🎬', desc: 'Media Streaming', url: '/mediaflow/', iconClass: 'apps' },
|
|
||||||
]
|
<div class="grid">
|
||||||
|
|
||||||
|
<!-- Vhosts (live-hosts top) -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>Top Vhosts (last 60 min)</h3>
|
||||||
|
<div id="vhosts-table"><div class="empty">Loading…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attackers (visitor-origin top ASNs) -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>Top Visitor ASN</h3>
|
||||||
|
<div id="asn-table"><div class="empty">Loading…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>SecuBox © 2024-2026 · <a href="https://cybermind.fr">CyberMind</a> · <a href="https://github.com/CyberMind-FR/secubox-deb">source</a></p>
|
||||||
|
<p style="margin-top: 0.5rem;">Public Portal · <span id="footer-hostname">—</span> · auto-refresh 30 s</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const REFRESH_MS = 30000;
|
||||||
|
const ENDPOINTS = {
|
||||||
|
health: '/api/v1/metrics/health/summary',
|
||||||
|
certs: '/api/v1/metrics/cert-status',
|
||||||
|
vhosts: '/api/v1/metrics/live-hosts',
|
||||||
|
asns: '/api/v1/metrics/visitor-origin',
|
||||||
|
cookies: '/api/v1/cookie-audit/summary',
|
||||||
|
decisions: '/api/v1/crowdsec/decisions',
|
||||||
|
};
|
||||||
|
|
||||||
|
function $(id) { return document.getElementById(id); }
|
||||||
|
function txt(id, v) { const e = $(id); if (e) e.textContent = (v === undefined || v === null) ? '—' : v; }
|
||||||
|
function fmtUptime(s) {
|
||||||
|
if (!s) return '—';
|
||||||
|
const d = Math.floor(s / 86400);
|
||||||
|
const h = Math.floor((s % 86400) / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
return `${d}d ${String(h).padStart(2,'0')}h${String(m).padStart(2,'0')}`;
|
||||||
|
}
|
||||||
|
function fmtPct(v) {
|
||||||
|
if (v === null || v === undefined) return '—';
|
||||||
|
return Number(v).toFixed(0) + '%';
|
||||||
|
}
|
||||||
|
function fmtTime(ts) {
|
||||||
|
if (!ts) return '—';
|
||||||
|
try { return new Date(ts).toLocaleTimeString('fr-FR'); } catch (e) { return '—'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSafe(url) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url, { credentials: 'omit' });
|
||||||
|
if (!r.ok) return null;
|
||||||
|
return await r.json();
|
||||||
|
} catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHero(h, c) {
|
||||||
|
if (!h) return;
|
||||||
|
const score = h.score || 0;
|
||||||
|
txt('hero-score-num', score + '%');
|
||||||
|
const sc = $('hero-score');
|
||||||
|
sc.classList.remove('warn', 'crit');
|
||||||
|
if (score < 70) sc.classList.add('crit');
|
||||||
|
else if (score < 85) sc.classList.add('warn');
|
||||||
|
txt('hero-hostname', location.hostname);
|
||||||
|
txt('nav-hostname', location.hostname);
|
||||||
|
txt('footer-hostname', location.hostname);
|
||||||
|
txt('hero-uptime', fmtUptime(h.system && h.system.uptime));
|
||||||
|
if (h.ssl) {
|
||||||
|
const d = h.ssl.days_remaining;
|
||||||
|
txt('hero-ssl', d == null ? '—' : `${d}j (${h.ssl.status || '?'})`);
|
||||||
|
}
|
||||||
|
txt('hero-vhosts', h.counts && h.counts.vhosts);
|
||||||
|
txt('hero-certs', (c && c.summary && c.summary.total) || (h.counts && h.counts.certificates) || '—');
|
||||||
|
txt('hero-bans', h.crowdsec && h.crowdsec.active_decisions);
|
||||||
|
txt('hero-refresh', fmtTime(h.timestamp || new Date().toISOString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderModules(h) {
|
||||||
|
if (!h || !h.modules) return;
|
||||||
|
const order = ['waf', 'crowdsec', 'haproxy', 'nginx', 'system'];
|
||||||
|
const icons = { waf: '🛡️', crowdsec: '👮', haproxy: '🌐', nginx: '📡', system: '💻' };
|
||||||
|
const el = $('modules-grid');
|
||||||
|
el.innerHTML = order.map(m => {
|
||||||
|
const mod = h.modules[m] || {};
|
||||||
|
const status = mod.status || 'off';
|
||||||
|
const cls = status === 'ok' ? 'ok' : status === 'warn' ? 'warn' : status === 'error' ? 'err' : '';
|
||||||
|
return `<div class="module ${cls}">
|
||||||
|
<span class="led"></span>
|
||||||
|
<span style="font-size:1.4rem;line-height:1">${icons[m]||'📦'}</span>
|
||||||
|
<span class="name">${m}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSystem(h) {
|
||||||
|
if (!h || !h.system) return;
|
||||||
|
const s = h.system;
|
||||||
|
const row = (k, v, pct) => {
|
||||||
|
const cls = pct == null ? '' : (pct >= 85 ? 'crit' : pct >= 60 ? 'warn' : 'ok');
|
||||||
|
return `<div class="row"><span class="k">${k}</span><span class="v ${cls}">${v}</span></div>` +
|
||||||
|
(pct != null ? `<div class="bar ${cls}"><span style="width:${Math.min(100, pct)}%"></span></div>` : '');
|
||||||
};
|
};
|
||||||
|
$('sys-rows').innerHTML =
|
||||||
|
row('CPU', fmtPct(s.cpu), s.cpu) +
|
||||||
|
row('Memory', fmtPct(s.memory || s.mem_pct), (s.memory || s.mem_pct)) +
|
||||||
|
row('Disk', fmtPct(s.disk), s.disk) +
|
||||||
|
row('Load', s.load || '—', null) +
|
||||||
|
row('LXC', (h.services && h.services.lxc_running) || 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
function renderService(svc, status = null) {
|
function renderCerts(c) {
|
||||||
const isOnline = status === null || status;
|
if (!c || !c.summary) {
|
||||||
const statusClass = svc.external ? 'external' : (isOnline ? 'online' : 'offline');
|
$('cert-rows').innerHTML = '<div class="empty">cert-status disabled</div>';
|
||||||
const statusText = svc.external ? 'External' : (isOnline ? 'Online' : 'Offline');
|
return;
|
||||||
return `
|
|
||||||
<a href="${svc.url}" class="service-card">
|
|
||||||
<div class="icon ${svc.iconClass}">${svc.icon}</div>
|
|
||||||
<div class="name">${svc.name}</div>
|
|
||||||
<div class="desc">${svc.desc}</div>
|
|
||||||
<div class="status ${statusClass}">${statusText}</div>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
const s = c.summary;
|
||||||
|
const next = c.next_renewal
|
||||||
|
? `<div class="row"><span class="k">next renewal</span><span class="v">${c.next_renewal.host} · ${c.next_renewal.days}d</span></div>`
|
||||||
|
: '';
|
||||||
|
$('cert-rows').innerHTML = `
|
||||||
|
<div class="row"><span class="k">Total</span><span class="v">${s.total || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Valid</span><span class="v ok">${s.valid || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Expiring soon</span><span class="v ${s.expiring_soon ? 'warn' : ''}">${s.expiring_soon || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Critical</span><span class="v ${s.expiring_critical ? 'crit' : ''}">${s.expiring_critical || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Expired</span><span class="v ${s.expired ? 'crit' : ''}">${s.expired || 0}</span></div>
|
||||||
|
${next}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderPublishedSite(site) {
|
function renderCookies(d) {
|
||||||
const icon = site.ssl ? '🔒' : '🌐';
|
if (!d || !d.enabled || !d.summary) {
|
||||||
const typeClass = site.type === 'static' ? 'static' : 'proxy';
|
$('cookie-rows').innerHTML = '<div class="empty">cookie audit disabled</div>';
|
||||||
return `
|
return;
|
||||||
<a href="https://${site.domain}" target="_blank" class="published-card">
|
|
||||||
<div class="title">${icon} ${site.name || site.domain}</div>
|
|
||||||
<div class="domain">${site.domain}</div>
|
|
||||||
<div class="meta">
|
|
||||||
<span class="badge ${site.ssl ? 'ssl' : ''}">${site.ssl ? 'SSL' : 'HTTP'}</span>
|
|
||||||
<span class="badge ${typeClass}">${site.type || 'site'}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
const s = d.summary;
|
||||||
|
const byCat = s.by_category || {};
|
||||||
|
const violCls = s.violation_count > 0 ? 'crit' : 'ok';
|
||||||
|
$('cookie-rows').innerHTML = `
|
||||||
|
<div class="row"><span class="k">Vhosts surveillés</span><span class="v">${s.host_count || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">RGPD violations</span><span class="v ${violCls}">${s.violation_count || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Strictly necessary</span><span class="v">${byCat.strictly_necessary || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Functional</span><span class="v">${byCat.functional || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Analytics</span><span class="v ${byCat.analytics ? 'warn' : ''}">${byCat.analytics || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Marketing</span><span class="v ${byCat.marketing ? 'warn' : ''}">${byCat.marketing || 0}</span></div>
|
||||||
|
<div class="row"><span class="k">Unclassified</span><span class="v ${byCat.unclassified ? 'warn' : ''}">${byCat.unclassified || 0}</span></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStats() {
|
function renderBans(h, decisions) {
|
||||||
try {
|
const ban = h && h.crowdsec && h.crowdsec.active_decisions;
|
||||||
// Health stats
|
const alerts = h && h.crowdsec && h.crowdsec.alerts_today;
|
||||||
const health = await fetch('/api/v1/metrics/health/summary').then(r => r.json()).catch(() => ({}));
|
const wafPct = h && h.waf && h.waf.blocked_pct;
|
||||||
document.getElementById('stat-health').textContent = health.score !== undefined ? health.score : '-';
|
const wafCls = wafPct == null ? '' : (wafPct >= 25 ? 'crit' : wafPct >= 10 ? 'warn' : 'ok');
|
||||||
|
const banCls = ban == null ? '' : (ban >= 50 ? 'warn' : 'ok');
|
||||||
|
$('t-bans').textContent = ban != null ? ban : '—';
|
||||||
|
$('t-bans').className = 'v ' + banCls;
|
||||||
|
$('t-waf').textContent = wafPct != null ? wafPct + '%' : '—';
|
||||||
|
$('t-waf').className = 'v ' + wafCls;
|
||||||
|
$('t-alerts').textContent = alerts != null ? alerts : '—';
|
||||||
|
}
|
||||||
|
|
||||||
// Services count
|
function renderVhosts(d) {
|
||||||
const c3box = await fetch('/api/v1/c3box/services').then(r => r.json()).catch(() => ({}));
|
if (!d || !d.enabled || !d.entries || !d.entries.length) {
|
||||||
document.getElementById('stat-services').textContent = c3box.running !== undefined ? `${c3box.running}/${c3box.total}` : '-';
|
$('vhosts-table').innerHTML = '<div class="empty">live-hosts disabled or no data</div>';
|
||||||
|
return;
|
||||||
// CrowdSec threats
|
|
||||||
const crowdsec = await fetch('/api/v1/crowdsec/stats').then(r => r.json()).catch(() => ({}));
|
|
||||||
document.getElementById('stat-threats').textContent = crowdsec.alerts_24h !== undefined ? crowdsec.alerts_24h : '-';
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Stats load error:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
$('vhosts-table').innerHTML = `<table>
|
||||||
|
<thead><tr><th>Host</th><th style="text-align:right">Requests</th></tr></thead>
|
||||||
|
<tbody>${d.entries.map(e => `<tr><td>${escapeHtml(e.host)}</td><td class="num">${e.count}</td></tr>`).join('')}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPublishedSites() {
|
function renderAsns(d) {
|
||||||
const container = document.getElementById('published-sites');
|
if (!d || !d.enabled || !d.entries || !d.entries.length) {
|
||||||
try {
|
$('asn-table').innerHTML = '<div class="empty">visitor-origin disabled (missing GeoLite2-ASN.mmdb)</div>';
|
||||||
// Try vhost API
|
return;
|
||||||
const vhosts = await fetch('/api/v1/vhost/list').then(r => r.json()).catch(() => ({ vhosts: [] }));
|
|
||||||
|
|
||||||
if (vhosts.vhosts && vhosts.vhosts.length > 0) {
|
|
||||||
const sites = vhosts.vhosts.map(v => ({
|
|
||||||
domain: v.domain || v.name,
|
|
||||||
name: v.name || v.domain,
|
|
||||||
ssl: v.ssl || v.https || false,
|
|
||||||
type: v.type || 'proxy'
|
|
||||||
}));
|
|
||||||
document.getElementById('stat-sites').textContent = sites.length;
|
|
||||||
container.innerHTML = sites.map(renderPublishedSite).join('');
|
|
||||||
} else {
|
|
||||||
// Fallback: try publish API
|
|
||||||
const publish = await fetch('/api/v1/publish/sites').then(r => r.json()).catch(() => ({ sites: [] }));
|
|
||||||
if (publish.sites && publish.sites.length > 0) {
|
|
||||||
document.getElementById('stat-sites').textContent = publish.sites.length;
|
|
||||||
container.innerHTML = publish.sites.map(s => renderPublishedSite({
|
|
||||||
domain: s.domain || s.url,
|
|
||||||
name: s.name || s.title,
|
|
||||||
ssl: true,
|
|
||||||
type: s.type || 'static'
|
|
||||||
})).join('');
|
|
||||||
} else {
|
|
||||||
document.getElementById('stat-sites').textContent = '0';
|
|
||||||
container.innerHTML = '<div class="empty">No published sites yet</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById('stat-sites').textContent = '-';
|
|
||||||
container.innerHTML = '<div class="empty">Could not load published sites</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
$('asn-table').innerHTML = `<table>
|
||||||
|
<thead><tr><th>ASN</th><th>Org</th><th style="text-align:right">Hits</th></tr></thead>
|
||||||
|
<tbody>${d.entries.map(e => `<tr><td>AS${e.asn}</td><td>${escapeHtml(e.org)}</td><td class="num">${e.count}</td></tr>`).join('')}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTheme() {
|
function escapeHtml(s) {
|
||||||
try {
|
const div = document.createElement('div');
|
||||||
const theme = await fetch('/api/v1/portal/theme').then(r => r.json()).catch(() => null);
|
div.textContent = s == null ? '' : String(s);
|
||||||
if (theme && theme.theme) {
|
return div.innerHTML;
|
||||||
document.getElementById('logo-text').textContent = theme.theme.logo_text || 'SecuBox';
|
}
|
||||||
document.getElementById('tagline').textContent = theme.theme.logo_sub || 'Secure Network Appliance';
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAuth() {
|
async function refresh() {
|
||||||
const token = document.cookie.includes('secubox_token');
|
const [h, c, v, a, k, dec] = await Promise.all([
|
||||||
if (token) {
|
fetchSafe(ENDPOINTS.health),
|
||||||
const btn = document.getElementById('auth-btn');
|
fetchSafe(ENDPOINTS.certs),
|
||||||
btn.textContent = '🚪 Logout';
|
fetchSafe(ENDPOINTS.vhosts),
|
||||||
btn.href = '#';
|
fetchSafe(ENDPOINTS.asns),
|
||||||
btn.onclick = async (e) => {
|
fetchSafe(ENDPOINTS.cookies),
|
||||||
e.preventDefault();
|
fetchSafe(ENDPOINTS.decisions),
|
||||||
await fetch('/api/v1/portal/logout', { method: 'POST', credentials: 'include' });
|
]);
|
||||||
location.reload();
|
renderHero(h, c);
|
||||||
};
|
renderModules(h);
|
||||||
}
|
renderSystem(h);
|
||||||
}
|
renderCerts(c);
|
||||||
|
renderCookies(k);
|
||||||
|
renderBans(h, dec);
|
||||||
|
renderVhosts(v);
|
||||||
|
renderAsns(a);
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
refresh();
|
||||||
document.getElementById('security-services').innerHTML = services.security.map(s => renderService(s)).join('');
|
setInterval(refresh, REFRESH_MS);
|
||||||
document.getElementById('network-services').innerHTML = services.network.map(s => renderService(s)).join('');
|
})();
|
||||||
document.getElementById('apps-services').innerHTML = services.apps.map(s => renderService(s)).join('');
|
</script>
|
||||||
document.getElementById('external-services').innerHTML = services.external.map(s => renderService(s)).join('');
|
|
||||||
document.getElementById('hostname').textContent = location.hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
render();
|
|
||||||
loadStats();
|
|
||||||
loadPublishedSites();
|
|
||||||
loadTheme();
|
|
||||||
checkAuth();
|
|
||||||
|
|
||||||
// Refresh stats every 30s
|
|
||||||
setInterval(loadStats, 30000);
|
|
||||||
</script>
|
|
||||||
<script src="/shared/health-banner.js" defer></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SecuBox MetaCtl — ISP Home Publish CLI
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
# CyberMind — https://cybermind.fr
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# publishctl — SecuBox ISP Home Publish CLI (issue #180)
|
||||||
|
#
|
||||||
|
# Renamed from `metactl` for naming consistency with the rest of the
|
||||||
|
# SecuBox grammar (haproxyctl/giteactl/mitmproxyctl/metablogizerctl/
|
||||||
|
# dropletctl/streamlitctl/streamforgectl). The old `metactl` name remains
|
||||||
|
# as a symlink for backward compatibility — to drop in a future major.
|
||||||
|
#
|
||||||
|
# Flat verbs are now also reachable under the `post` noun dispatch
|
||||||
|
# for grammar consistency (publishctl post upload <file>, etc). Flat
|
||||||
|
# top-level verbs preserved for backward compatibility.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="1.0.0"
|
VERSION="2.0.0"
|
||||||
API_BASE="${SECUBOX_API_BASE:-http://127.0.0.1/api/v1/publish}"
|
API_BASE="${SECUBOX_API_BASE:-http://127.0.0.1/api/v1/publish}"
|
||||||
METABLOGIZER_API="${SECUBOX_METABLOGIZER_API:-http://127.0.0.1/api/v1/metablogizer}"
|
METABLOGIZER_API="${SECUBOX_METABLOGIZER_API:-http://127.0.0.1/api/v1/metablogizer}"
|
||||||
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
TOKEN_FILE="${SECUBOX_TOKEN_FILE:-/etc/secubox/secrets/jwt-token}"
|
||||||
|
|
@ -408,8 +419,39 @@ cmd_health() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# post noun dispatch (issue #180 — grammar consistency, parallel to
|
||||||
|
# `giteactl repo`, `mitmproxyctl route`, `dropletctl file`, etc).
|
||||||
|
# Delegates to the existing flat cmd_* functions; both grammars supported.
|
||||||
|
cmd_post() {
|
||||||
|
local act="${1:-}"; shift || true
|
||||||
|
case "$act" in
|
||||||
|
upload) cmd_upload "$@" ;;
|
||||||
|
publish) cmd_publish "$@" ;;
|
||||||
|
unpublish) cmd_unpublish "$@" ;;
|
||||||
|
list|ls) cmd_list ;;
|
||||||
|
download) cmd_download "$@" ;;
|
||||||
|
qrcode|qr) cmd_qrcode "$@" ;;
|
||||||
|
health) cmd_health "$@" ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Post commands (issue #180):
|
||||||
|
post upload <file.zip> [name] [--domain=D] [--auto-publish]
|
||||||
|
post publish <name>
|
||||||
|
post unpublish <name>
|
||||||
|
post list
|
||||||
|
post download <name> [output.zip]
|
||||||
|
post qrcode <name>
|
||||||
|
post health <domain>
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
|
# noun-verb grammar (issue #180)
|
||||||
|
post) shift; cmd_post "$@" ;;
|
||||||
|
# flat verbs (backward-compat — same callbacks)
|
||||||
upload) shift; cmd_upload "$@" ;;
|
upload) shift; cmd_upload "$@" ;;
|
||||||
publish) shift; cmd_publish "$@" ;;
|
publish) shift; cmd_publish "$@" ;;
|
||||||
unpublish) shift; cmd_unpublish "$@" ;;
|
unpublish) shift; cmd_unpublish "$@" ;;
|
||||||
|
|
@ -419,10 +461,10 @@ case "${1:-help}" in
|
||||||
status) cmd_status ;;
|
status) cmd_status ;;
|
||||||
health) shift; cmd_health "$@" ;;
|
health) shift; cmd_health "$@" ;;
|
||||||
-h|--help|help) usage ;;
|
-h|--help|help) usage ;;
|
||||||
-v|--version) echo "metactl v${VERSION}" ;;
|
-v|--version) echo "publishctl v${VERSION}" ;;
|
||||||
*)
|
*)
|
||||||
echo -e "${RED}Unknown command:${NC} $1"
|
echo -e "${RED}Unknown command:${NC} $1"
|
||||||
echo "Run 'metactl --help' for usage"
|
echo "Run 'publishctl --help' for usage"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
secubox-publish (2.0.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Rename `metactl` -> `publishctl` for naming consistency with the rest
|
||||||
|
of the SecuBox ctl grammar (issue #180). The `metactl` name remains
|
||||||
|
as a symlink for backward compatibility — to drop in a future major.
|
||||||
|
* publishctl: add `post` noun dispatch so verbs are grouped under a
|
||||||
|
coherent <noun> <verb> schema parallel to giteactl/dropletctl/
|
||||||
|
metablogizerctl. Flat top-level verbs preserved as alias.
|
||||||
|
|
||||||
|
publishctl post upload <file.zip> [name] [--auto-publish]
|
||||||
|
publishctl post publish/unpublish <name>
|
||||||
|
publishctl post list/download/qrcode/health ...
|
||||||
|
|
||||||
|
* Bumped to 2.0.0 (CLI surface rename).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:38:19 +0200
|
||||||
|
|
||||||
secubox-publish (1.0.0-1~bookworm1) bookworm; urgency=medium
|
secubox-publish (1.0.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-publish/etc/nginx/secubox.d
|
install -d debian/secubox-publish/etc/nginx/secubox.d
|
||||||
[ -f nginx/publish.conf ] && cp nginx/publish.conf debian/secubox-publish/etc/nginx/secubox.d/ || true
|
[ -f nginx/publish.conf ] && cp nginx/publish.conf debian/secubox-publish/etc/nginx/secubox.d/ || true
|
||||||
# CLI tool
|
# CLI tool — primary `publishctl` + `metactl` symlink for backward compat (#180)
|
||||||
install -d debian/secubox-publish/usr/sbin
|
install -d debian/secubox-publish/usr/sbin
|
||||||
[ -f bin/metactl ] && install -m 755 bin/metactl debian/secubox-publish/usr/sbin/metactl || true
|
[ -f bin/publishctl ] && install -m 755 bin/publishctl debian/secubox-publish/usr/sbin/publishctl || true
|
||||||
|
[ -f debian/secubox-publish/usr/sbin/publishctl ] && \
|
||||||
|
ln -sf publishctl debian/secubox-publish/usr/sbin/metactl || true
|
||||||
# Plugins directory
|
# Plugins directory
|
||||||
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
install -d debian/secubox-publish/srv/secubox/modules/publish/plugins
|
||||||
[ -d plugins ] && cp -r plugins/. debian/secubox-publish/srv/secubox/modules/publish/plugins/ || true
|
[ -d plugins ] && cp -r plugins/. debian/secubox-publish/srv/secubox/modules/publish/plugins/ || true
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
secubox-streamforge (1.0.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Forge streamforgectl (issue #183). Subcommands: lifecycle (start/stop/
|
||||||
|
restart/status/logs), Three-fold JSON (components/access), project
|
||||||
|
noun (create/remove/list/start/stop/restart/info/templates) wrapping
|
||||||
|
the /api/v1/streamforge/app* endpoints over the Unix socket.
|
||||||
|
* Paired with streamlitctl on the hosting side — forge -> host workflow
|
||||||
|
expressible end-to-end (forge create/edit -> export to git -> stream
|
||||||
|
deploy).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:34:28 +0200
|
||||||
|
|
||||||
secubox-streamforge (1.0.1-1~bookworm1) bookworm; urgency=medium
|
secubox-streamforge (1.0.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Add dynamic menu system with menu.d JSON definitions
|
* Add dynamic menu system with menu.d JSON definitions
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,6 @@ override_dh_auto_install:
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-streamforge/etc/nginx/secubox.d
|
install -d debian/secubox-streamforge/etc/nginx/secubox.d
|
||||||
[ -f nginx/streamforge.conf ] && cp nginx/streamforge.conf debian/secubox-streamforge/etc/nginx/secubox.d/ || true
|
[ -f nginx/streamforge.conf ] && cp nginx/streamforge.conf debian/secubox-streamforge/etc/nginx/secubox.d/ || true
|
||||||
|
# streamforgectl (#183)
|
||||||
|
install -d debian/secubox-streamforge/usr/sbin
|
||||||
|
install -m 755 sbin/streamforgectl debian/secubox-streamforge/usr/sbin/
|
||||||
|
|
|
||||||
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
143
packages/secubox-streamforge/sbin/streamforgectl
Executable file
|
|
@ -0,0 +1,143 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
# streamforgectl — SecuBox StreamForge (Streamlit dev workbench) control (#183)
|
||||||
|
#
|
||||||
|
# Parallel to streamlitctl (the hosting side). The Forge ↔ Streamlit pair:
|
||||||
|
# streamforgectl project create <name> --template hello
|
||||||
|
# streamforgectl project export <name> <gitea_url>
|
||||||
|
# streamlitctl app deploy <name> <gitea_url>
|
||||||
|
# Three verbs, two layers (dev → hosting), one expressible workflow.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SOCKET="${STREAMFORGE_SOCKET:-/run/secubox/streamforge.sock}"
|
||||||
|
API="http://localhost/api/v1/streamforge"
|
||||||
|
SERVICE="secubox-streamforge.service"
|
||||||
|
|
||||||
|
G='\033[0;32m'; Y='\033[1;33m'; R='\033[0;31m'; N='\033[0m'
|
||||||
|
log() { printf "${G}[FORGE]${N} %s\n" "$*"; }
|
||||||
|
warn() { printf "${Y}[WARN]${N} %s\n" "$*"; }
|
||||||
|
error() { printf "${R}[ERROR]${N} %s\n" "$*" >&2; }
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local m="$1" p="$2"; shift 2
|
||||||
|
[ -S "$SOCKET" ] || { error "socket $SOCKET absent — start $SERVICE"; exit 2; }
|
||||||
|
curl --unix-socket "$SOCKET" -sS -X "$m" -w "\nHTTP_CODE:%{http_code}\n" "${API}${p}" "$@"
|
||||||
|
}
|
||||||
|
api_code() { echo "$1" | grep '^HTTP_CODE:' | cut -d: -f2; }
|
||||||
|
api_body() { echo "$1" | sed '/^HTTP_CODE:/d'; }
|
||||||
|
|
||||||
|
# Lifecycle
|
||||||
|
cmd_start() { systemctl start "$SERVICE" && log started; }
|
||||||
|
cmd_stop() { systemctl stop "$SERVICE" && log stopped; }
|
||||||
|
cmd_restart() { systemctl restart "$SERVICE" && log restarted; }
|
||||||
|
cmd_status() { systemctl is-active "$SERVICE" >/dev/null && echo active || echo inactive; }
|
||||||
|
cmd_logs() { journalctl -u "$SERVICE" -n "${1:-50}" --no-pager; }
|
||||||
|
|
||||||
|
cmd_components() {
|
||||||
|
cat <<EOF
|
||||||
|
{"service":"$SERVICE","socket":"$SOCKET","api_base":"$API","ctl_version":"$VERSION"}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
cmd_access() {
|
||||||
|
cat <<EOF
|
||||||
|
{"socket":"$SOCKET","endpoints":{
|
||||||
|
"apps":"GET /apps","templates":"GET /templates",
|
||||||
|
"create":"POST /app","get":"GET /app/{name}","remove":"DELETE /app/{name}",
|
||||||
|
"start":"POST /app/{name}/start","stop":"POST /app/{name}/stop","restart":"POST /app/{name}/restart",
|
||||||
|
"file_get":"GET /app/{name}/file/{path}","file_put":"PUT /app/{name}/file/{path}"
|
||||||
|
}}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Project (the noun, #183)
|
||||||
|
cmd_project() {
|
||||||
|
local act="${1:-}"; shift || true
|
||||||
|
case "$act" in
|
||||||
|
create) project_create "$@" ;;
|
||||||
|
remove|rm|delete) project_remove "$@" ;;
|
||||||
|
list|ls) project_list "$@" ;;
|
||||||
|
start) project_start "$@" ;;
|
||||||
|
stop) project_stop "$@" ;;
|
||||||
|
restart) project_restart "$@" ;;
|
||||||
|
info) project_info "$@" ;;
|
||||||
|
templates) project_templates ;;
|
||||||
|
*)
|
||||||
|
cat <<EOF
|
||||||
|
Project commands:
|
||||||
|
project create <name> [--template hello] [--description "..."]
|
||||||
|
project remove <name>
|
||||||
|
project list
|
||||||
|
project start <name> (start the project's streamlit dev server)
|
||||||
|
project stop <name>
|
||||||
|
project restart <name>
|
||||||
|
project info <name>
|
||||||
|
project templates (list available templates)
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
project_create() {
|
||||||
|
local name="$1"; shift || { error "project create <name> required"; return 1; }
|
||||||
|
local template="hello" desc=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--template) template="$2"; shift ;;
|
||||||
|
--description) desc="$2"; shift ;;
|
||||||
|
*) error "unknown flag: $1"; return 1 ;;
|
||||||
|
esac; shift
|
||||||
|
done
|
||||||
|
log "creating project '$name' from template '$template'"
|
||||||
|
local body
|
||||||
|
body=$(printf '{"name":"%s","template":"%s","description":"%s"}' "$name" "$template" "$desc")
|
||||||
|
local out; out=$(api POST "/app" -H "Content-Type: application/json" -d "$body")
|
||||||
|
[ "$(api_code "$out")" = "200" ] || { error "create failed: $(api_body "$out" | head -2)"; return 1; }
|
||||||
|
log "created"; api_body "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
project_remove() {
|
||||||
|
local name="$1"; [ -z "$name" ] && { error "project remove <name>"; return 1; }
|
||||||
|
local out; out=$(api DELETE "/app/${name}")
|
||||||
|
case "$(api_code "$out")" in
|
||||||
|
200|204) log "removed $name" ;;
|
||||||
|
*) error "remove failed: $(api_body "$out")"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
project_list() { api_body "$(api GET "/apps")"; }
|
||||||
|
|
||||||
|
project_start() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/start")"; }
|
||||||
|
project_stop() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/stop")"; }
|
||||||
|
project_restart() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api POST "/app/${n}/restart")"; }
|
||||||
|
project_info() { local n="$1"; [ -z "$n" ] && return 1; api_body "$(api GET "/app/${n}")"; }
|
||||||
|
project_templates() { api_body "$(api GET "/templates")"; }
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
SecuBox StreamForge Controller v$VERSION (issue #183)
|
||||||
|
Streamlit dev workbench CLI — paired with streamlitctl (hosting layer)
|
||||||
|
|
||||||
|
Lifecycle: start / stop / restart / status / logs
|
||||||
|
Three-fold: components / access (JSON)
|
||||||
|
Project (#183): project create / remove / list / start / stop / restart / info / templates
|
||||||
|
|
||||||
|
Example workflow (forge → host):
|
||||||
|
streamforgectl project create dashboard --template basic
|
||||||
|
streamforgectl project start dashboard
|
||||||
|
# ...iterate via the webui at /streamforge/...
|
||||||
|
streamforgectl project export dashboard gitea://secubox/dashboard.git # TODO
|
||||||
|
streamlitctl app deploy dashboard gitea://secubox/dashboard.git
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start|stop|restart|status) c="$1"; shift; cmd_$c "$@" ;;
|
||||||
|
logs) shift; cmd_logs "$@" ;;
|
||||||
|
components) cmd_components ;;
|
||||||
|
access) cmd_access ;;
|
||||||
|
project) shift; cmd_project "$@" ;;
|
||||||
|
help|--help|-h|'') show_help ;;
|
||||||
|
*) error "unknown: $1"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
|
secubox-streamlit (1.2.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* streamlitctl: add `app info <name>` and `app restart <name>` verbs
|
||||||
|
(issue #182). The original audit underestimated the existing ctl
|
||||||
|
surface — app list/start/stop/deploy/remove/logs were already wired.
|
||||||
|
What was actually missing: `info` (metadata + runtime state) and
|
||||||
|
`restart` (stop+start with port preservation from .streamlit.toml).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 17 May 2026 11:36:06 +0200
|
||||||
|
|
||||||
secubox-streamlit (1.2.0-1~bookworm1) bookworm; urgency=medium
|
secubox-streamlit (1.2.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* streamlitctl v1.0.0: Full Debian LXC installation support
|
* streamlitctl v1.0.0: Full Debian LXC installation support
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,51 @@ EOF
|
||||||
echo ']}'
|
echo ']}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# app info <name> — print metadata + runtime state from manifest + pid file (#182)
|
||||||
|
cmd_app_info() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: streamlitctl app info <name>"; return 1; }
|
||||||
|
local d="$APPS_PATH/$name"
|
||||||
|
[ -d "$d" ] || { error "app not found: $name"; return 1; }
|
||||||
|
# Entry point detection (same logic as cmd_app_start)
|
||||||
|
local entry=""
|
||||||
|
for c in app.py main.py streamlit_app.py; do
|
||||||
|
[ -f "$d/$c" ] && entry="$c" && break
|
||||||
|
done
|
||||||
|
local port=""
|
||||||
|
[ -f "$d/.streamlit.toml" ] && port=$(grep -E "^port" "$d/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||||
|
local pidf="/var/run/streamlit-${name}.pid"
|
||||||
|
local pid="" alive="no"
|
||||||
|
if lxc_running; then
|
||||||
|
pid=$(lxc-attach -n "$LXC_NAME" -- cat "$pidf" 2>/dev/null || true)
|
||||||
|
if [ -n "$pid" ]; then
|
||||||
|
lxc-attach -n "$LXC_NAME" -- kill -0 "$pid" >/dev/null 2>&1 && alive="yes"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cat <<EOF
|
||||||
|
name: $name
|
||||||
|
path: $d
|
||||||
|
entrypoint: ${entry:-(none)}
|
||||||
|
port: ${port:-(unset)}
|
||||||
|
pid_file: $pidf
|
||||||
|
pid: ${pid:-(none)}
|
||||||
|
running: $alive
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# app restart <name> — stop + start (#182)
|
||||||
|
cmd_app_restart() {
|
||||||
|
local name="$1"
|
||||||
|
[ -z "$name" ] && { error "Usage: streamlitctl app restart <name>"; return 1; }
|
||||||
|
cmd_app_stop "$name" || true
|
||||||
|
sleep 1
|
||||||
|
# Recover the previous port from .streamlit.toml so restart preserves it
|
||||||
|
local port=""
|
||||||
|
[ -f "$APPS_PATH/$name/.streamlit.toml" ] && \
|
||||||
|
port=$(grep -E "^port" "$APPS_PATH/$name/.streamlit.toml" 2>/dev/null | cut -d= -f2 | tr -d ' ')
|
||||||
|
cmd_app_start "$name" "${port:-8501}"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_app_start() {
|
cmd_app_start() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local port="${2:-8501}"
|
local port="${2:-8501}"
|
||||||
|
|
@ -694,7 +739,9 @@ case "${1:-}" in
|
||||||
deploy) cmd_app_deploy "$3" "$4" ;;
|
deploy) cmd_app_deploy "$3" "$4" ;;
|
||||||
remove) cmd_app_remove "$3" ;;
|
remove) cmd_app_remove "$3" ;;
|
||||||
logs) cmd_app_logs "$3" "$4" ;;
|
logs) cmd_app_logs "$3" "$4" ;;
|
||||||
*) echo "Usage: streamlitctl app {list|start|stop|deploy|remove|logs} [args]" ;;
|
info) cmd_app_info "$3" ;;
|
||||||
|
restart) cmd_app_restart "$3" ;;
|
||||||
|
*) echo "Usage: streamlitctl app {list|start|stop|restart|deploy|remove|logs|info} [args]" ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,16 +178,30 @@ check_prerequisites() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Charger les modules nécessaires
|
# Charger les modules nécessaires. dwc2 must be loaded FIRST — it creates
|
||||||
|
# the UDC node that the gadget functions bind to. Historically dwc2 was
|
||||||
|
# loaded implicitly by secubox-eye-gadget.service's ExecStartPre; with
|
||||||
|
# that service disabled at boot (storage-only mode is opt-in), the
|
||||||
|
# gadget chain now owns its own dwc2 modprobe explicitly.
|
||||||
|
modprobe dwc2 2>/dev/null || true
|
||||||
modprobe libcomposite 2>/dev/null || true
|
modprobe libcomposite 2>/dev/null || true
|
||||||
modprobe usb_f_ecm 2>/dev/null || true
|
modprobe usb_f_ecm 2>/dev/null || true
|
||||||
modprobe usb_f_rndis 2>/dev/null || true
|
modprobe usb_f_rndis 2>/dev/null || true
|
||||||
modprobe usb_f_acm 2>/dev/null || true
|
modprobe usb_f_acm 2>/dev/null || true
|
||||||
modprobe usb_f_mass_storage 2>/dev/null || true
|
modprobe usb_f_mass_storage 2>/dev/null || true
|
||||||
|
|
||||||
|
# dwc2 binds asynchronously to the BCM USB controller — wait up to 5s
|
||||||
|
# for the UDC node to appear (typically <500ms on a Pi Zero W).
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
if [[ -d /sys/class/udc ]] && [[ -n "$(ls /sys/class/udc 2>/dev/null)" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
# Vérifier la présence d'un UDC (USB Device Controller)
|
# Vérifier la présence d'un UDC (USB Device Controller)
|
||||||
if [[ ! -d /sys/class/udc ]] || [[ -z "$(ls /sys/class/udc 2>/dev/null)" ]]; then
|
if [[ ! -d /sys/class/udc ]] || [[ -z "$(ls /sys/class/udc 2>/dev/null)" ]]; then
|
||||||
err "Aucun UDC trouvé — ce script doit être exécuté sur un RPi Zero W"
|
err "Aucun UDC trouvé — vérifier dtoverlay=dwc2 dans /boot/config.txt et module dwc2 dans /etc/modules"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,10 +93,15 @@ LOGO_PATHS = [
|
||||||
Path("/etc/secubox/eye-remote/assets/phoenix_logo.png"),
|
Path("/etc/secubox/eye-remote/assets/phoenix_logo.png"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Icon paths - module icons
|
# Icon paths - module icons.
|
||||||
|
# Order matters: first existing path wins per icon name. /var/www/common/
|
||||||
|
# holds the real brand icons (auth, wall, boot, mind, root, mesh) installed
|
||||||
|
# by build-eye-remote-image.sh; the local /usr/lib/secubox-eye/assets/icons/
|
||||||
|
# fallback path holds the round-specific placeholder set.
|
||||||
ICON_PATHS = [
|
ICON_PATHS = [
|
||||||
Path("/tmp/assets/icons"),
|
Path("/tmp/assets/icons"),
|
||||||
Path("/etc/secubox/eye-remote/assets/icons"),
|
Path("/etc/secubox/eye-remote/assets/icons"),
|
||||||
|
Path("/var/www/common/assets/icons"),
|
||||||
Path(__file__).parent.parent.parent.parent / "assets" / "icons",
|
Path(__file__).parent.parent.parent.parent / "assets" / "icons",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -650,9 +650,14 @@ mkdir -p "$ROOT_MNT/etc/systemd/system/dnsmasq.service.d"
|
||||||
cp "$SCRIPT_DIR/files/etc/systemd/system/dnsmasq.service.d/secubox-eye.conf" \
|
cp "$SCRIPT_DIR/files/etc/systemd/system/dnsmasq.service.d/secubox-eye.conf" \
|
||||||
"$ROOT_MNT/etc/systemd/system/dnsmasq.service.d/"
|
"$ROOT_MNT/etc/systemd/system/dnsmasq.service.d/"
|
||||||
|
|
||||||
# Enable new gadget service
|
# secubox-eye-gadget is STORAGE-only mode for U-Boot rescue. Enabling it
|
||||||
ln -sf /etc/systemd/system/secubox-eye-gadget.service \
|
# at boot alongside secubox-otg-gadget.service (composite ECM+ACM) caused
|
||||||
"$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
|
# UDC contention. Install the unit but DO NOT enable at boot.
|
||||||
|
# Manual U-Boot rescue mode:
|
||||||
|
# systemctl disable secubox-otg-gadget.service
|
||||||
|
# systemctl enable --now secubox-eye-gadget.service
|
||||||
|
# ln -sf /etc/systemd/system/secubox-eye-gadget.service \
|
||||||
|
# "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
|
||||||
|
|
||||||
# Copy framebuffer dashboard (Pi Zero W has no NEON, can't run Chromium)
|
# Copy framebuffer dashboard (Pi Zero W has no NEON, can't run Chromium)
|
||||||
log "Installing framebuffer dashboard..."
|
log "Installing framebuffer dashboard..."
|
||||||
|
|
@ -684,18 +689,34 @@ if [[ -f "$SCRIPT_DIR/secubox-eye-agent.service" && -f "$SCRIPT_DIR/config.toml.
|
||||||
# Install agent service
|
# Install agent service
|
||||||
cp "$SCRIPT_DIR/secubox-eye-agent.service" "$ROOT_MNT/etc/systemd/system/"
|
cp "$SCRIPT_DIR/secubox-eye-agent.service" "$ROOT_MNT/etc/systemd/system/"
|
||||||
|
|
||||||
# Enable agent service via symlink (atomic, no chroot needed)
|
# The agent depends on Pydantic v2 (pydantic_core, Rust) which has no
|
||||||
|
# ARMv6 wheel — pip ships an ARMv7 wheel that crashes with SIGILL on
|
||||||
|
# the Pi Zero W BCM2835 (status=4/ILL). v2.2.1 design moved metrics
|
||||||
|
# rendering to secubox-fallback-display.service (pure-Python Pillow),
|
||||||
|
# so we install the unit but DO NOT enable at boot. ARMv7+ boards:
|
||||||
|
# systemctl enable --now secubox-eye-agent.service
|
||||||
mkdir -p "$ROOT_MNT/etc/systemd/system/multi-user.target.wants"
|
mkdir -p "$ROOT_MNT/etc/systemd/system/multi-user.target.wants"
|
||||||
ln -sf /etc/systemd/system/secubox-eye-agent.service \
|
# ln -sf /etc/systemd/system/secubox-eye-agent.service \
|
||||||
"$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
|
# "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/"
|
||||||
|
|
||||||
# v2.2.0: Install menu system icons for radial menu
|
# v2.2.0: Install menu system icons for radial menu
|
||||||
if [[ -d "$SCRIPT_DIR/assets/icons" ]]; then
|
if [[ -d "$SCRIPT_DIR/assets/icons" ]]; then
|
||||||
log "Installing menu system icons..."
|
log "Installing menu system icons..."
|
||||||
mkdir -p "$ROOT_MNT/usr/lib/secubox-eye/assets/icons"
|
mkdir -p "$ROOT_MNT/usr/lib/secubox-eye/assets/icons"
|
||||||
cp "$SCRIPT_DIR/assets/icons"/*.png "$ROOT_MNT/usr/lib/secubox-eye/assets/icons/" 2>/dev/null || true
|
cp "$SCRIPT_DIR/assets/icons"/*.png "$ROOT_MNT/usr/lib/secubox-eye/assets/icons/" 2>/dev/null || true
|
||||||
|
# Defense-in-depth: also drop the brand-icon PNGs (auth/wall/boot/
|
||||||
|
# mind/root/mesh) from remote-ui/common/assets/icons/ so any consumer
|
||||||
|
# that resolves icons via /usr/lib/secubox-eye/ finds them. The
|
||||||
|
# fallback_manager also searches /var/www/common/assets/icons/
|
||||||
|
# directly (where build copies common/ in another step) — this is
|
||||||
|
# redundant shipping for resilience.
|
||||||
|
_COMMON_ICONS="$(dirname "$SCRIPT_DIR")/common/assets/icons"
|
||||||
|
if [[ -d "$_COMMON_ICONS" ]]; then
|
||||||
|
cp -n "$_COMMON_ICONS"/*.png \
|
||||||
|
"$ROOT_MNT/usr/lib/secubox-eye/assets/icons/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
ICON_COUNT=$(ls "$ROOT_MNT/usr/lib/secubox-eye/assets/icons"/*.png 2>/dev/null | wc -l)
|
ICON_COUNT=$(ls "$ROOT_MNT/usr/lib/secubox-eye/assets/icons"/*.png 2>/dev/null | wc -l)
|
||||||
log "Installed $ICON_COUNT menu icons"
|
log "Installed $ICON_COUNT menu icons (round/ + common/ brand)"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "Eye Agent files not found, skipping installation"
|
warn "Eye Agent files not found, skipping installation"
|
||||||
|
|
@ -766,7 +787,10 @@ ln -sf /etc/systemd/system/eye-firstboot-hostname.service "$ROOT_MNT/etc/systemd
|
||||||
ln -sf /etc/systemd/system/hyperpixel2r-init.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
ln -sf /etc/systemd/system/hyperpixel2r-init.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
||||||
|
|
||||||
# Eye Remote services
|
# Eye Remote services
|
||||||
ln -sf /etc/systemd/system/secubox-eye-gadget.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
# secubox-eye-gadget (storage-only, U-Boot rescue) is NOT enabled at boot
|
||||||
|
# to avoid UDC contention with secubox-otg-gadget.service. See comment at
|
||||||
|
# line 653 for the manual rescue-mode recipe.
|
||||||
|
# ln -sf /etc/systemd/system/secubox-eye-gadget.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
||||||
# v2.2.1: Use fallback-display instead of eye-agent (3D cube + rainbow rings, stable)
|
# v2.2.1: Use fallback-display instead of eye-agent (3D cube + rainbow rings, stable)
|
||||||
ln -sf /etc/systemd/system/secubox-fallback-display.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
ln -sf /etc/systemd/system/secubox-fallback-display.service "$ROOT_MNT/etc/systemd/system/multi-user.target.wants/" 2>/dev/null || true
|
||||||
# NOTE: secubox-eye-agent is broken (import errors) - disabled pending fix
|
# NOTE: secubox-eye-agent is broken (import errors) - disabled pending fix
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,19 @@ After=systemd-modules-load.service
|
||||||
Before=network-pre.target
|
Before=network-pre.target
|
||||||
Wants=network-pre.target
|
Wants=network-pre.target
|
||||||
|
|
||||||
# Conditions : seulement sur un périphérique avec UDC (RPi Zero W)
|
# Conditions : configfs disponible (UDC est crééable dynamiquement via dwc2,
|
||||||
ConditionPathIsDirectory=/sys/class/udc
|
# voir ExecStartPre — pas de pré-condition stricte sur /sys/class/udc).
|
||||||
ConditionPathExists=/sys/kernel/config
|
ConditionPathExists=/sys/kernel/config
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
|
|
||||||
# Chargement des modules nécessaires
|
# Chargement des modules nécessaires. dwc2 est chargé en premier pour créer
|
||||||
|
# le UDC (sinon le gadget ne peut s'attacher). secubox-eye-gadget.service
|
||||||
|
# le chargeait historiquement avant; depuis qu'il est désactivé au boot
|
||||||
|
# (storage-only mode opt-in), on doit le charger ici.
|
||||||
|
ExecStartPre=/sbin/modprobe dwc2
|
||||||
ExecStartPre=/sbin/modprobe libcomposite
|
ExecStartPre=/sbin/modprobe libcomposite
|
||||||
ExecStartPre=/sbin/modprobe usb_f_ecm
|
ExecStartPre=/sbin/modprobe usb_f_ecm
|
||||||
ExecStartPre=/sbin/modprobe usb_f_acm
|
ExecStartPre=/sbin/modprobe usb_f_acm
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user