New meta-status report combining dev + services with enhanced visuals: - Stats rings with conic gradients (health, services, uptime) - Channel distribution bars (Tor/DNS/Mesh percentages) - Stat cards with icons and gradients - Recent completions and WIP sections - Roadmap progress visualization - Top services tables Email configuration: - Default to local mailserver (127.0.0.1:25) - Default recipient: gk2@secubox.in - No TLS for local delivery CLI: secubox-reportctl generate meta Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
540 lines
17 KiB
Bash
540 lines
17 KiB
Bash
#!/bin/sh
|
|
# SecuBox Report Generator
|
|
# Generates and distributes status reports via HTML and email
|
|
# Copyright (C) 2026 CyberMind.fr
|
|
|
|
VERSION="1.0.0"
|
|
SCRIPT_DIR="/usr/share/secubox-reporter"
|
|
LIB_DIR="$SCRIPT_DIR/lib"
|
|
TPL_DIR="$SCRIPT_DIR/templates"
|
|
OUTPUT_DIR="/www/reports"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
# Load libraries
|
|
. /lib/functions.sh
|
|
[ -f "$LIB_DIR/collectors.sh" ] && . "$LIB_DIR/collectors.sh"
|
|
[ -f "$LIB_DIR/formatters.sh" ] && . "$LIB_DIR/formatters.sh"
|
|
[ -f "$LIB_DIR/mailer.sh" ] && . "$LIB_DIR/mailer.sh"
|
|
|
|
# Load config
|
|
config_load secubox-reporter
|
|
config_get OUTPUT_DIR global output_dir "/www/reports"
|
|
config_get THEME global theme "dark"
|
|
config_get REPO_PATH sources repo_path "/root/secubox-openwrt"
|
|
|
|
log_info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
|
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
log_err() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
|
|
usage() {
|
|
cat << EOF
|
|
SecuBox Report Generator v$VERSION
|
|
|
|
Usage: secubox-reportctl <command> [options]
|
|
|
|
COMMANDS:
|
|
generate <type> Generate report (dev|services|all)
|
|
send <type> Generate and email report
|
|
schedule <type> Set cron schedule (daily|weekly|off)
|
|
status Show generator status
|
|
preview <type> Generate and output to stdout
|
|
list List generated reports
|
|
clean [days] Remove reports older than N days (default: 30)
|
|
help Show this help
|
|
|
|
REPORT TYPES:
|
|
dev Development Status Report (progress, roadmap, health)
|
|
services Distribution/Services Status Report (exposures, channels)
|
|
all Both reports
|
|
|
|
OPTIONS:
|
|
--email Also send via email
|
|
--theme <name> Theme: dark, light, cyberpunk (default: dark)
|
|
--output <path> Custom output path
|
|
--json Output status as JSON
|
|
|
|
EXAMPLES:
|
|
secubox-reportctl generate dev
|
|
secubox-reportctl send services
|
|
secubox-reportctl schedule dev daily
|
|
secubox-reportctl generate all --theme cyberpunk
|
|
EOF
|
|
}
|
|
|
|
# Get hostname
|
|
get_hostname() {
|
|
uci -q get system.@system[0].hostname 2>/dev/null || hostname
|
|
}
|
|
|
|
# Get SecuBox version
|
|
get_version() {
|
|
cat /etc/secubox_version 2>/dev/null || echo "0.19.x"
|
|
}
|
|
|
|
# Generate development status report
|
|
generate_dev_report() {
|
|
local output_file="$1"
|
|
local theme="${2:-dark}"
|
|
|
|
log_info "Generating Development Status Report..."
|
|
|
|
mkdir -p "$(dirname "$output_file")"
|
|
|
|
local hostname=$(get_hostname)
|
|
local version=$(get_version)
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local health_score=0
|
|
|
|
# Get health score from RPCD if available
|
|
if command -v ubus >/dev/null 2>&1; then
|
|
health_score=$(ubus call luci.secubox-core get_full_health_report 2>/dev/null | \
|
|
jsonfilter -e '@.health_score' 2>/dev/null || echo "0")
|
|
fi
|
|
[ -z "$health_score" ] && health_score=0
|
|
|
|
# Collect data from .claude files
|
|
local history_entries=""
|
|
local wip_entries=""
|
|
local roadmap_data=""
|
|
|
|
if [ -d "$REPO_PATH/.claude" ]; then
|
|
# Recent history (last 10 dated sections)
|
|
history_entries=$(collect_history "$REPO_PATH/.claude/HISTORY.md")
|
|
|
|
# WIP items
|
|
wip_entries=$(collect_wip "$REPO_PATH/.claude/WIP.md")
|
|
|
|
# Roadmap progress
|
|
roadmap_data=$(collect_roadmap "$REPO_PATH/.claude/ROADMAP.md")
|
|
else
|
|
history_entries="<p class='muted'>Repository not found at $REPO_PATH</p>"
|
|
wip_entries="<p class='muted'>Repository not found</p>"
|
|
roadmap_data="<p class='muted'>Repository not found</p>"
|
|
fi
|
|
|
|
# Generate HTML from template
|
|
if [ -f "$TPL_DIR/dev-status.html.tpl" ]; then
|
|
sed -e "s|{{HOSTNAME}}|$hostname|g" \
|
|
-e "s|{{VERSION}}|$version|g" \
|
|
-e "s|{{TIMESTAMP}}|$timestamp|g" \
|
|
-e "s|{{HEALTH_SCORE}}|$health_score|g" \
|
|
-e "s|{{THEME}}|$theme|g" \
|
|
-e "s|{{HISTORY_ENTRIES}}|$history_entries|g" \
|
|
-e "s|{{WIP_ENTRIES}}|$wip_entries|g" \
|
|
-e "s|{{ROADMAP_DATA}}|$roadmap_data|g" \
|
|
"$TPL_DIR/dev-status.html.tpl" > "$output_file"
|
|
else
|
|
# Fallback inline template
|
|
generate_dev_html_inline "$output_file" "$hostname" "$version" "$timestamp" \
|
|
"$health_score" "$history_entries" "$wip_entries" "$roadmap_data"
|
|
fi
|
|
|
|
# Ensure web-readable permissions
|
|
chmod 644 "$output_file"
|
|
|
|
log_ok "Generated: $output_file"
|
|
echo "$output_file"
|
|
}
|
|
|
|
# Generate services status report
|
|
generate_services_report() {
|
|
local output_file="$1"
|
|
local theme="${2:-dark}"
|
|
|
|
log_info "Generating Services Status Report..."
|
|
|
|
mkdir -p "$(dirname "$output_file")"
|
|
|
|
local hostname=$(get_hostname)
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
# Count services by channel
|
|
local tor_count=0
|
|
local dns_count=0
|
|
local mesh_count=0
|
|
|
|
# Tor hidden services
|
|
if [ -d /var/lib/tor/hidden_services ]; then
|
|
tor_count=$(ls -1 /var/lib/tor/hidden_services 2>/dev/null | wc -l)
|
|
fi
|
|
|
|
# HAProxy vhosts
|
|
dns_count=$(uci show haproxy 2>/dev/null | grep '=vhost$' | wc -l)
|
|
|
|
# Mesh services (from P2P if available)
|
|
if command -v secubox-p2p >/dev/null 2>&1; then
|
|
mesh_count=$(secubox-p2p shared-services 2>/dev/null | wc -l)
|
|
fi
|
|
|
|
# Collect service details
|
|
local tor_services=$(collect_tor_services)
|
|
local dns_services=$(collect_dns_services)
|
|
local mesh_services=$(collect_mesh_services)
|
|
|
|
# Health check
|
|
local health_up=0
|
|
local health_total=$((tor_count + dns_count))
|
|
|
|
# Generate HTML
|
|
if [ -f "$TPL_DIR/services-status.html.tpl" ]; then
|
|
sed -e "s|{{HOSTNAME}}|$hostname|g" \
|
|
-e "s|{{TIMESTAMP}}|$timestamp|g" \
|
|
-e "s|{{TOR_COUNT}}|$tor_count|g" \
|
|
-e "s|{{DNS_COUNT}}|$dns_count|g" \
|
|
-e "s|{{MESH_COUNT}}|$mesh_count|g" \
|
|
-e "s|{{HEALTH_UP}}|$health_up|g" \
|
|
-e "s|{{HEALTH_TOTAL}}|$health_total|g" \
|
|
-e "s|{{TOR_SERVICES}}|$tor_services|g" \
|
|
-e "s|{{DNS_SERVICES}}|$dns_services|g" \
|
|
-e "s|{{MESH_SERVICES}}|$mesh_services|g" \
|
|
-e "s|{{THEME}}|$theme|g" \
|
|
"$TPL_DIR/services-status.html.tpl" > "$output_file"
|
|
else
|
|
generate_services_html_inline "$output_file" "$hostname" "$timestamp" \
|
|
"$tor_count" "$dns_count" "$mesh_count" \
|
|
"$tor_services" "$dns_services" "$mesh_services"
|
|
fi
|
|
|
|
# Ensure web-readable permissions
|
|
chmod 644 "$output_file"
|
|
|
|
log_ok "Generated: $output_file"
|
|
echo "$output_file"
|
|
}
|
|
|
|
# Generate meta status report (combined dashboard)
|
|
generate_meta_report() {
|
|
local output_file="$1"
|
|
local theme="${2:-dark}"
|
|
|
|
log_info "Generating Meta Status Report..."
|
|
|
|
mkdir -p "$(dirname "$output_file")"
|
|
|
|
local hostname=$(get_hostname)
|
|
local version=$(get_version)
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
# Health score
|
|
local health_score=0
|
|
if command -v ubus >/dev/null 2>&1; then
|
|
health_score=$(ubus call luci.secubox-core get_full_health_report 2>/dev/null | \
|
|
jsonfilter -e '@.health_score' 2>/dev/null || echo "0")
|
|
fi
|
|
[ -z "$health_score" ] && health_score=0
|
|
|
|
# Service counts
|
|
local tor_count=0
|
|
local dns_count=0
|
|
local mesh_count=0
|
|
|
|
[ -d /var/lib/tor/hidden_services ] && tor_count=$(ls -1 /var/lib/tor/hidden_services 2>/dev/null | wc -l)
|
|
dns_count=$(uci show haproxy 2>/dev/null | grep '=vhost$' | wc -l)
|
|
command -v secubox-p2p >/dev/null 2>&1 && mesh_count=$(secubox-p2p shared-services 2>/dev/null | wc -l)
|
|
|
|
local total_services=$((tor_count + dns_count + mesh_count))
|
|
local services_pct=100
|
|
[ $total_services -gt 0 ] && services_pct=$((total_services * 100 / 300))
|
|
[ $services_pct -gt 100 ] && services_pct=100
|
|
|
|
# Calculate percentages for bars
|
|
local tor_pct=0 dns_pct=0 mesh_pct=0
|
|
[ $total_services -gt 0 ] && {
|
|
tor_pct=$((tor_count * 100 / total_services))
|
|
dns_pct=$((dns_count * 100 / total_services))
|
|
mesh_pct=$((mesh_count * 100 / total_services))
|
|
}
|
|
|
|
# System stats
|
|
local packages_count=$(opkg list-installed 2>/dev/null | wc -l)
|
|
local containers_count=$(lxc-ls 2>/dev/null | wc -w)
|
|
local uptime_pct=99
|
|
|
|
# Dev stats
|
|
local features_done=0
|
|
local wip_count=0
|
|
[ -f "$REPO_PATH/.claude/HISTORY.md" ] && features_done=$(grep -c "^\*\*" "$REPO_PATH/.claude/HISTORY.md" 2>/dev/null || echo 0)
|
|
[ -f "$REPO_PATH/.claude/WIP.md" ] && wip_count=$(grep -c "^- \*\*" "$REPO_PATH/.claude/WIP.md" 2>/dev/null || echo 0)
|
|
|
|
# Collect formatted data
|
|
local history_entries=$(collect_history "$REPO_PATH/.claude/HISTORY.md" 2>/dev/null || echo '<p class="muted">No history data</p>')
|
|
local wip_entries=$(collect_wip "$REPO_PATH/.claude/WIP.md" 2>/dev/null || echo '<p class="muted">No WIP data</p>')
|
|
local roadmap_data=$(collect_roadmap "$REPO_PATH/.claude/ROADMAP.md" 2>/dev/null || echo '<p class="muted">No roadmap data</p>')
|
|
local tor_services=$(collect_tor_services 2>/dev/null || echo '<p class="muted">No Tor services</p>')
|
|
local dns_services=$(collect_dns_services 10 2>/dev/null || echo '<p class="muted">No DNS services</p>')
|
|
|
|
# Generate from template
|
|
if [ -f "$TPL_DIR/meta-status.html.tpl" ]; then
|
|
sed -e "s|{{HOSTNAME}}|$hostname|g" \
|
|
-e "s|{{VERSION}}|$version|g" \
|
|
-e "s|{{TIMESTAMP}}|$timestamp|g" \
|
|
-e "s|{{HEALTH_SCORE}}|$health_score|g" \
|
|
-e "s|{{TOTAL_SERVICES}}|$total_services|g" \
|
|
-e "s|{{SERVICES_PCT}}|$services_pct|g" \
|
|
-e "s|{{UPTIME_PCT}}|$uptime_pct|g" \
|
|
-e "s|{{TOR_COUNT}}|$tor_count|g" \
|
|
-e "s|{{DNS_COUNT}}|$dns_count|g" \
|
|
-e "s|{{MESH_COUNT}}|$mesh_count|g" \
|
|
-e "s|{{TOR_PCT}}|$tor_pct|g" \
|
|
-e "s|{{DNS_PCT}}|$dns_pct|g" \
|
|
-e "s|{{MESH_PCT}}|$mesh_pct|g" \
|
|
-e "s|{{PACKAGES_COUNT}}|$packages_count|g" \
|
|
-e "s|{{CONTAINERS_COUNT}}|$containers_count|g" \
|
|
-e "s|{{FEATURES_DONE}}|$features_done|g" \
|
|
-e "s|{{WIP_COUNT}}|$wip_count|g" \
|
|
-e "s|{{HISTORY_ENTRIES}}|$history_entries|g" \
|
|
-e "s|{{WIP_ENTRIES}}|$wip_entries|g" \
|
|
-e "s|{{ROADMAP_DATA}}|$roadmap_data|g" \
|
|
-e "s|{{TOR_SERVICES}}|$tor_services|g" \
|
|
-e "s|{{DNS_SERVICES}}|$dns_services|g" \
|
|
"$TPL_DIR/meta-status.html.tpl" > "$output_file"
|
|
else
|
|
log_err "Meta template not found: $TPL_DIR/meta-status.html.tpl"
|
|
return 1
|
|
fi
|
|
|
|
chmod 644 "$output_file"
|
|
log_ok "Generated: $output_file"
|
|
echo "$output_file"
|
|
}
|
|
|
|
# Command: generate
|
|
cmd_generate() {
|
|
local report_type="$1"
|
|
local theme="$THEME"
|
|
local output_path="$OUTPUT_DIR"
|
|
|
|
shift
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--theme) theme="$2"; shift 2 ;;
|
|
--output) output_path="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
local timestamp=$(date '+%Y%m%d-%H%M%S')
|
|
|
|
case "$report_type" in
|
|
dev)
|
|
generate_dev_report "$output_path/dev-status-$timestamp.html" "$theme"
|
|
;;
|
|
services)
|
|
generate_services_report "$output_path/services-status-$timestamp.html" "$theme"
|
|
;;
|
|
meta)
|
|
generate_meta_report "$output_path/meta-status-$timestamp.html" "$theme"
|
|
;;
|
|
all)
|
|
generate_dev_report "$output_path/dev-status-$timestamp.html" "$theme"
|
|
generate_services_report "$output_path/services-status-$timestamp.html" "$theme"
|
|
;;
|
|
*)
|
|
log_err "Unknown report type: $report_type"
|
|
echo "Valid types: dev, services, meta, all"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Command: send
|
|
cmd_send() {
|
|
local report_type="$1"
|
|
shift
|
|
|
|
# Generate report first
|
|
local report_file=$(cmd_generate "$report_type" "$@")
|
|
|
|
if [ -z "$report_file" ] || [ ! -f "$report_file" ]; then
|
|
log_err "Failed to generate report"
|
|
return 1
|
|
fi
|
|
|
|
# Send via email
|
|
local recipient=""
|
|
config_get recipient email recipient ""
|
|
|
|
if [ -z "$recipient" ]; then
|
|
log_warn "No email recipient configured. Report saved to: $report_file"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Sending report to $recipient..."
|
|
|
|
if send_report_email "$report_type" "$(cat "$report_file")" "$recipient"; then
|
|
log_ok "Report sent to $recipient"
|
|
else
|
|
log_err "Failed to send email"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Command: schedule
|
|
cmd_schedule() {
|
|
local report_type="$1"
|
|
local frequency="$2"
|
|
local cron_file="/etc/cron.d/secubox-reporter"
|
|
|
|
case "$frequency" in
|
|
daily)
|
|
sed -i "s/^#DAILY_${report_type^^}#//" "$cron_file" 2>/dev/null
|
|
uci set secubox-reporter.schedule.$report_type='daily'
|
|
;;
|
|
weekly)
|
|
sed -i "s/^#WEEKLY_${report_type^^}#//" "$cron_file" 2>/dev/null
|
|
uci set secubox-reporter.schedule.$report_type='weekly'
|
|
;;
|
|
off)
|
|
# Re-comment the lines
|
|
sed -i "/secubox-reportctl.*$report_type/s/^[^#]/#DISABLED#/" "$cron_file" 2>/dev/null
|
|
uci set secubox-reporter.schedule.$report_type='off'
|
|
;;
|
|
*)
|
|
log_err "Invalid frequency: $frequency"
|
|
echo "Valid: daily, weekly, off"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
uci commit secubox-reporter
|
|
/etc/init.d/cron restart 2>/dev/null || true
|
|
log_ok "Schedule set: $report_type = $frequency"
|
|
}
|
|
|
|
# Command: status
|
|
cmd_status() {
|
|
local json_output=0
|
|
[ "$1" = "--json" ] && json_output=1
|
|
|
|
local hostname=$(get_hostname)
|
|
local version=$(get_version)
|
|
local dev_schedule=$(uci -q get secubox-reporter.schedule.dev || echo "off")
|
|
local services_schedule=$(uci -q get secubox-reporter.schedule.services || echo "off")
|
|
local recipient=$(uci -q get secubox-reporter.email.recipient || echo "")
|
|
local report_count=$(ls -1 "$OUTPUT_DIR"/*.html 2>/dev/null | wc -l)
|
|
|
|
if [ $json_output -eq 1 ]; then
|
|
cat << EOF
|
|
{"hostname":"$hostname","version":"$version","schedules":{"dev":"$dev_schedule","services":"$services_schedule"},"email_recipient":"$recipient","report_count":$report_count}
|
|
EOF
|
|
else
|
|
echo "SecuBox Report Generator v$VERSION"
|
|
echo "=================================="
|
|
echo "Hostname: $hostname"
|
|
echo "Version: $version"
|
|
echo "Output Dir: $OUTPUT_DIR"
|
|
echo "Reports: $report_count files"
|
|
echo ""
|
|
echo "Schedules:"
|
|
echo " Dev: $dev_schedule"
|
|
echo " Services: $services_schedule"
|
|
echo ""
|
|
echo "Email: ${recipient:-Not configured}"
|
|
fi
|
|
}
|
|
|
|
# Command: list
|
|
cmd_list() {
|
|
local json_output=0
|
|
[ "$1" = "--json" ] && json_output=1
|
|
|
|
if [ $json_output -eq 1 ]; then
|
|
echo '{"reports":['
|
|
local first=1
|
|
for f in $(ls -1t "$OUTPUT_DIR"/*.html 2>/dev/null); do
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
local name=$(basename "$f")
|
|
local size=$(stat -c%s "$f" 2>/dev/null || echo 0)
|
|
local mtime=$(stat -c%Y "$f" 2>/dev/null || echo 0)
|
|
echo "{\"name\":\"$name\",\"size\":$size,\"mtime\":$mtime}"
|
|
done
|
|
echo ']}'
|
|
else
|
|
echo "Generated Reports:"
|
|
echo "=================="
|
|
ls -lh "$OUTPUT_DIR"/*.html 2>/dev/null || echo "No reports found"
|
|
fi
|
|
}
|
|
|
|
# Command: clean
|
|
cmd_clean() {
|
|
local days="${1:-30}"
|
|
|
|
log_info "Removing reports older than $days days..."
|
|
|
|
local count=$(find "$OUTPUT_DIR" -name "*.html" -mtime +$days 2>/dev/null | wc -l)
|
|
find "$OUTPUT_DIR" -name "*.html" -mtime +$days -delete 2>/dev/null
|
|
|
|
log_ok "Removed $count old reports"
|
|
}
|
|
|
|
# Command: preview
|
|
cmd_preview() {
|
|
local report_type="$1"
|
|
local tmpfile="/tmp/secubox-report-preview-$$.html"
|
|
|
|
case "$report_type" in
|
|
dev)
|
|
generate_dev_report "$tmpfile" "$THEME" >/dev/null
|
|
;;
|
|
services)
|
|
generate_services_report "$tmpfile" "$THEME" >/dev/null
|
|
;;
|
|
*)
|
|
log_err "Unknown report type: $report_type"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
cat "$tmpfile"
|
|
rm -f "$tmpfile"
|
|
}
|
|
|
|
# Main
|
|
case "$1" in
|
|
generate)
|
|
shift
|
|
cmd_generate "$@"
|
|
;;
|
|
send)
|
|
shift
|
|
cmd_send "$@"
|
|
;;
|
|
schedule)
|
|
shift
|
|
cmd_schedule "$@"
|
|
;;
|
|
status)
|
|
shift
|
|
cmd_status "$@"
|
|
;;
|
|
list)
|
|
shift
|
|
cmd_list "$@"
|
|
;;
|
|
clean)
|
|
shift
|
|
cmd_clean "$@"
|
|
;;
|
|
preview)
|
|
shift
|
|
cmd_preview "$@"
|
|
;;
|
|
help|--help|-h|"")
|
|
usage
|
|
;;
|
|
*)
|
|
log_err "Unknown command: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|