feat(secubox-app-reporter): Add report generator for dev and services status

Two-report system for SecuBox status reporting:
- Development Status Report: health score, HISTORY.md completions, WIP items, roadmap progress
- Services Distribution Report: Tor hidden services, DNS/SSL vhosts, mesh services

Features:
- CLI: secubox-reportctl generate|send|schedule|status|preview|list|clean
- HTML output with KissTheme dark styling
- Email delivery via msmtp/sendmail with MIME multipart
- UCI configuration for SMTP and scheduling
- Cron integration for automated reports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-13 09:18:36 +01:00
parent 019e181890
commit 246f2acc18
10 changed files with 1293 additions and 0 deletions

View File

@ -0,0 +1,51 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-reporter
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=GPL-3.0
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-reporter
SECTION:=secubox
CATEGORY:=SecuBox
TITLE:=SecuBox Report Generator
DEPENDS:=+secubox-core +curl +msmtp
PKGARCH:=all
endef
define Package/secubox-app-reporter/description
Generates and distributes SecuBox status reports via HTML and email.
Includes development progress tracking and services distribution status.
endef
define Package/secubox-app-reporter/conffiles
/etc/config/secubox-reporter
endef
define Build/Compile
endef
define Package/secubox-app-reporter/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/secubox-reporter $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/cron.d
$(INSTALL_DATA) ./files/etc/cron.d/secubox-reporter $(1)/etc/cron.d/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/secubox-reportctl $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/share/secubox-reporter/lib
$(INSTALL_DATA) ./files/usr/share/secubox-reporter/lib/*.sh $(1)/usr/share/secubox-reporter/lib/
$(INSTALL_DIR) $(1)/usr/share/secubox-reporter/templates
$(INSTALL_DATA) ./files/usr/share/secubox-reporter/templates/*.tpl $(1)/usr/share/secubox-reporter/templates/
$(INSTALL_DIR) $(1)/www/reports
endef
$(eval $(call BuildPackage,secubox-app-reporter))

View File

@ -0,0 +1,26 @@
config global 'global'
option enabled '1'
option theme 'dark'
option output_dir '/www/reports'
option retention_days '30'
config email 'email'
option enabled '0'
option recipient ''
option smtp_server ''
option smtp_port '587'
option smtp_user ''
option smtp_password ''
option smtp_tls '1'
config schedule 'schedule'
option dev 'off'
option services 'off'
config sources 'sources'
option repo_path '/root/secubox-openwrt'
option history_file '.claude/HISTORY.md'
option wip_file '.claude/WIP.md'
option todo_file '.claude/TODO.md'
option roadmap_file '.claude/ROADMAP.md'
option devstatus_file '.claude/DEV-STATUS.md'

View File

@ -0,0 +1,11 @@
# SecuBox Report Generator - Scheduled Reports
# Managed by: secubox-reportctl schedule
# Daily development report at 6 AM (disabled by default)
#DAILY_DEV#0 6 * * * root /usr/sbin/secubox-reportctl send dev >/dev/null 2>&1
# Weekly services report on Monday at 7 AM (disabled by default)
#WEEKLY_SERVICES#0 7 * * 1 root /usr/sbin/secubox-reportctl send services >/dev/null 2>&1
# Monthly full report on 1st at 8 AM (disabled by default)
#MONTHLY_ALL#0 8 1 * * root /usr/sbin/secubox-reportctl send all >/dev/null 2>&1

View File

@ -0,0 +1,434 @@
#!/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
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
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"
;;
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, 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

View File

@ -0,0 +1,185 @@
#!/bin/sh
# SecuBox Reporter - Data Collectors
# Extracts data from .claude files and system services
# Collect recent history entries
collect_history() {
local history_file="$1"
[ ! -f "$history_file" ] && return
local output=""
local count=0
local max_entries=10
local current_date=""
local in_section=0
while IFS= read -r line; do
# Match date headers like "### 2026-03-12"
if echo "$line" | grep -qE '^### [0-9]{4}-[0-9]{2}-[0-9]{2}'; then
[ $count -ge $max_entries ] && break
current_date=$(echo "$line" | sed 's/### //')
output="$output<div class='history-entry'><div class='history-date'>$current_date</div><ul>"
in_section=1
count=$((count + 1))
elif [ $in_section -eq 1 ]; then
# Match bullet points
if echo "$line" | grep -qE '^- '; then
local item=$(echo "$line" | sed 's/^- //' | sed 's/\*\*\([^*]*\)\*\*/<strong>\1<\/strong>/g')
output="$output<li>$item</li>"
elif echo "$line" | grep -qE '^$'; then
output="$output</ul></div>"
in_section=0
fi
fi
done < "$history_file"
# Close any open section
[ $in_section -eq 1 ] && output="$output</ul></div>"
echo "$output"
}
# Collect WIP items
collect_wip() {
local wip_file="$1"
[ ! -f "$wip_file" ] && return
local output=""
local in_nextup=0
local in_inprogress=0
while IFS= read -r line; do
# Find "Next Up" or "In Progress" sections
if echo "$line" | grep -qiE '^## Next Up|^### Next Up'; then
output="$output<div class='wip-section'><h4>Next Up</h4><ul>"
in_nextup=1
in_inprogress=0
elif echo "$line" | grep -qiE '^## In Progress|^### In Progress'; then
[ $in_nextup -eq 1 ] && output="$output</ul></div>"
output="$output<div class='wip-section'><h4>In Progress</h4><ul>"
in_inprogress=1
in_nextup=0
elif echo "$line" | grep -qE '^## |^### '; then
# End of section
[ $in_nextup -eq 1 ] && output="$output</ul></div>"
[ $in_inprogress -eq 1 ] && output="$output</ul></div>"
in_nextup=0
in_inprogress=0
elif [ $in_nextup -eq 1 ] || [ $in_inprogress -eq 1 ]; then
if echo "$line" | grep -qE '^[0-9]+\. |^- '; then
local item=$(echo "$line" | sed 's/^[0-9]*\. //' | sed 's/^- //' | sed 's/\*\*\([^*]*\)\*\*/<strong>\1<\/strong>/g')
[ -n "$item" ] && output="$output<li>$item</li>"
fi
fi
done < "$wip_file"
# Close any open sections
[ $in_nextup -eq 1 ] && output="$output</ul></div>"
[ $in_inprogress -eq 1 ] && output="$output</ul></div>"
echo "$output"
}
# Collect roadmap progress
collect_roadmap() {
local roadmap_file="$1"
[ ! -f "$roadmap_file" ] && return
local output=""
local current_version=""
# Extract version sections with status
while IFS= read -r line; do
if echo "$line" | grep -qE '^### v[0-9]+\.[0-9]+'; then
current_version=$(echo "$line" | grep -oE 'v[0-9]+\.[0-9]+')
local title=$(echo "$line" | sed 's/^### //')
output="$output<div class='roadmap-version'><h4>$title</h4>"
elif echo "$line" | grep -qiE '^\*\*Status:'; then
local status=$(echo "$line" | sed 's/.*Status: *//' | sed 's/\*//g')
local status_class="pending"
echo "$status" | grep -qi "progress" && status_class="inprogress"
echo "$status" | grep -qi "complete\|done" && status_class="complete"
output="$output<span class='status-badge $status_class'>$status</span></div>"
fi
done < "$roadmap_file"
echo "$output"
}
# Collect Tor hidden services
collect_tor_services() {
local output=""
local tor_dir="/var/lib/tor/hidden_services"
[ ! -d "$tor_dir" ] && echo "<p class='muted'>No Tor services configured</p>" && return
output="<table class='services-table'><thead><tr><th>Service</th><th>Onion Address</th><th>Port</th></tr></thead><tbody>"
for service_dir in "$tor_dir"/*/; do
[ ! -d "$service_dir" ] && continue
local name=$(basename "$service_dir")
local hostname_file="$service_dir/hostname"
if [ -f "$hostname_file" ]; then
local onion=$(cat "$hostname_file" | head -1)
local port=$(grep -oE '[0-9]+$' "$service_dir/../torrc" 2>/dev/null | head -1)
[ -z "$port" ] && port="80"
output="$output<tr><td>$name</td><td><code>$onion</code></td><td>$port</td></tr>"
fi
done
output="$output</tbody></table>"
echo "$output"
}
# Collect DNS/SSL services (HAProxy vhosts)
collect_dns_services() {
local output=""
output="<table class='services-table'><thead><tr><th>Domain</th><th>Backend</th><th>SSL</th><th>Status</th></tr></thead><tbody>"
# Parse HAProxy vhosts from UCI
for vhost in $(uci show haproxy 2>/dev/null | grep '=vhost$' | cut -d'.' -f2 | cut -d'=' -f1); do
local enabled=$(uci -q get haproxy.$vhost.enabled)
[ "$enabled" != "1" ] && continue
local domain=$(uci -q get haproxy.$vhost.domain)
local backend=$(uci -q get haproxy.$vhost.backend)
local ssl=$(uci -q get haproxy.$vhost.ssl)
local ssl_icon="❌"
[ "$ssl" = "1" ] && ssl_icon="✅"
local status_class="running"
local status_text="Active"
output="$output<tr><td><a href='https://$domain' target='_blank'>$domain</a></td><td>$backend</td><td>$ssl_icon</td><td><span class='status-badge $status_class'>$status_text</span></td></tr>"
done
output="$output</tbody></table>"
echo "$output"
}
# Collect Mesh services
collect_mesh_services() {
local output=""
if ! command -v secubox-p2p >/dev/null 2>&1; then
echo "<p class='muted'>P2P mesh not installed</p>"
return
fi
output="<table class='services-table'><thead><tr><th>Service</th><th>Node</th><th>Port</th></tr></thead><tbody>"
# Get shared services from P2P
secubox-p2p shared-services 2>/dev/null | while read line; do
local name=$(echo "$line" | cut -d':' -f1)
local node=$(echo "$line" | cut -d':' -f2)
local port=$(echo "$line" | cut -d':' -f3)
output="$output<tr><td>$name</td><td>$node</td><td>$port</td></tr>"
done
output="$output</tbody></table>"
echo "$output"
}

View File

@ -0,0 +1,228 @@
#!/bin/sh
# SecuBox Reporter - HTML Formatters
# Inline HTML generation for reports
# KissTheme CSS (embedded)
get_kisstheme_css() {
cat << 'CSS'
:root{
--bg:#0a0a0f;--surface:#12121a;--card:#1a1a2e;
--ink:#f0f2ff;--dim:rgba(240,242,255,.5);--muted:#666;
--primary:#6366f1;--primary-end:#8b5cf6;
--cyan:#06b6d4;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;
--glass:rgba(255,255,255,.04);--border:rgba(255,255,255,.08);
}
*{margin:0;padding:0;box-sizing:border-box}
body{min-height:100vh;background:var(--bg);color:var(--ink);font-family:"Inter","Segoe UI",system-ui,sans-serif;padding:2rem;line-height:1.6}
.container{max-width:1200px;margin:0 auto}
h1{font-size:2rem;margin-bottom:.5rem;background:linear-gradient(90deg,var(--primary),var(--primary-end),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
h2{font-size:1.25rem;color:var(--cyan);margin:1.5rem 0 1rem;padding-bottom:.5rem;border-bottom:1px solid var(--border)}
h3{font-size:1rem;color:var(--ink);margin-bottom:.5rem}
h4{font-size:.9rem;color:var(--dim);margin-bottom:.5rem}
.header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:2rem;padding:1.5rem;background:var(--card);border:1px solid var(--border);border-radius:12px}
.header-info{flex:1}
.header-meta{display:flex;gap:1rem;flex-wrap:wrap}
.meta-item{font-size:.75rem;color:var(--muted);padding:.25rem .75rem;background:var(--glass);border-radius:4px}
.score-badge{font-size:2rem;font-weight:700;padding:1rem 1.5rem;background:linear-gradient(135deg,var(--primary),var(--primary-end));border-radius:12px;text-align:center;min-width:100px}
.score-label{font-size:.6rem;text-transform:uppercase;letter-spacing:.1em;opacity:.7;display:block;margin-top:.25rem}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
.stat-badge{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:1rem;text-align:center}
.stat-value{font-size:1.5rem;font-weight:700;background:linear-gradient(90deg,var(--primary),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.stat-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:.25rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;margin-bottom:1.5rem}
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
.card-title{font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}
.card-title::before{content:"";width:3px;height:1em;background:var(--primary);border-radius:2px}
ul{list-style:none;padding-left:0}
li{padding:.5rem 0;border-bottom:1px solid var(--border);font-size:.9rem}
li:last-child{border-bottom:none}
li strong{color:var(--cyan)}
.history-entry{margin-bottom:1.5rem}
.history-date{font-size:.8rem;color:var(--primary);font-weight:600;margin-bottom:.5rem;padding:.25rem .5rem;background:rgba(99,102,241,.1);border-radius:4px;display:inline-block}
.wip-section{margin-bottom:1rem}
.wip-section h4{color:var(--cyan);font-size:.85rem;margin-bottom:.5rem}
.roadmap-version{display:flex;align-items:center;gap:1rem;padding:.75rem;background:var(--glass);border-radius:8px;margin-bottom:.5rem}
.roadmap-version h4{margin:0;flex:1}
.status-badge{font-size:.65rem;padding:.25rem .5rem;border-radius:4px;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
.status-badge.complete,.status-badge.running{background:rgba(34,197,94,.15);color:var(--green)}
.status-badge.inprogress{background:rgba(99,102,241,.15);color:var(--primary)}
.status-badge.pending{background:rgba(102,102,102,.15);color:var(--muted)}
.services-table{width:100%;border-collapse:collapse;font-size:.85rem}
.services-table th,.services-table td{padding:.75rem;text-align:left;border-bottom:1px solid var(--border)}
.services-table th{color:var(--dim);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
.services-table a{color:var(--cyan);text-decoration:none}
.services-table a:hover{text-decoration:underline}
.services-table code{font-size:.75rem;padding:.15rem .35rem;background:var(--glass);border-radius:3px;font-family:"JetBrains Mono",monospace}
.muted{color:var(--muted);font-style:italic}
footer{margin-top:3rem;text-align:center;color:var(--muted);font-size:.75rem;padding-top:1.5rem;border-top:1px solid var(--border)}
footer a{color:var(--primary)}
@media(max-width:768px){body{padding:1rem}.header{flex-direction:column}.stats-grid{grid-template-columns:repeat(2,1fr)}}
CSS
}
# Generate development report HTML inline
generate_dev_html_inline() {
local output_file="$1"
local hostname="$2"
local version="$3"
local timestamp="$4"
local health_score="$5"
local history_entries="$6"
local wip_entries="$7"
local roadmap_data="$8"
cat > "$output_file" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Development Status - $hostname</title>
<style>
$(get_kisstheme_css)
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<h1>Development Status Report</h1>
<div class="header-meta">
<span class="meta-item">$hostname</span>
<span class="meta-item">v$version</span>
<span class="meta-item">$timestamp</span>
</div>
</div>
<div class="score-badge">
$health_score%
<span class="score-label">Health Score</span>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Completions</h3>
</div>
<div class="card-body">
$history_entries
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Work In Progress</h3>
</div>
<div class="card-body">
$wip_entries
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Roadmap Progress</h3>
</div>
<div class="card-body">
$roadmap_data
</div>
</div>
<footer>
Generated by SecuBox Report Generator v1.0 | <a href="/cgi-bin/luci/admin/secubox/reporter">LuCI Dashboard</a>
</footer>
</div>
</body>
</html>
EOF
}
# Generate services report HTML inline
generate_services_html_inline() {
local output_file="$1"
local hostname="$2"
local timestamp="$3"
local tor_count="$4"
local dns_count="$5"
local mesh_count="$6"
local tor_services="$7"
local dns_services="$8"
local mesh_services="$9"
local total=$((tor_count + dns_count + mesh_count))
cat > "$output_file" << EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Services Status - $hostname</title>
<style>
$(get_kisstheme_css)
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<h1>Distribution Status Report</h1>
<div class="header-meta">
<span class="meta-item">$hostname</span>
<span class="meta-item">$timestamp</span>
</div>
</div>
<div class="score-badge">
$total
<span class="score-label">Published Spaces</span>
</div>
</div>
<div class="stats-grid">
<div class="stat-badge">
<div class="stat-value">$tor_count</div>
<div class="stat-label">Tor Services</div>
</div>
<div class="stat-badge">
<div class="stat-value">$dns_count</div>
<div class="stat-label">DNS/SSL Vhosts</div>
</div>
<div class="stat-badge">
<div class="stat-value">$mesh_count</div>
<div class="stat-label">Mesh Services</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Tor Hidden Services</h3>
</div>
<div class="card-body">
$tor_services
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">DNS/SSL Services</h3>
</div>
<div class="card-body">
$dns_services
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Mesh Services</h3>
</div>
<div class="card-body">
$mesh_services
</div>
</div>
<footer>
Generated by SecuBox Report Generator v1.0 | <a href="/cgi-bin/luci/admin/secubox/reporter">LuCI Dashboard</a>
</footer>
</div>
</body>
</html>
EOF
}

View File

@ -0,0 +1,128 @@
#!/bin/sh
# SecuBox Reporter - Email Integration
# Sends reports via msmtp or sendmail
. /lib/functions.sh
# Send report via email
send_report_email() {
local report_type="$1"
local html_content="$2"
local recipient="$3"
local smtp_server=""
local smtp_port=""
local smtp_user=""
local smtp_password=""
local smtp_tls=""
config_load secubox-reporter
config_get smtp_server email smtp_server ""
config_get smtp_port email smtp_port "587"
config_get smtp_user email smtp_user ""
config_get smtp_password email smtp_password ""
config_get smtp_tls email smtp_tls "1"
[ -z "$recipient" ] && {
echo "ERROR: No email recipient specified" >&2
return 1
}
local hostname=$(uci -q get system.@system[0].hostname || hostname)
local date_str=$(date '+%Y-%m-%d')
# Format report type for subject
local report_name="Status"
case "$report_type" in
dev) report_name="Development Status" ;;
services) report_name="Services Distribution" ;;
all) report_name="Full Status" ;;
esac
local subject="[SecuBox] $report_name Report - $hostname - $date_str"
# Build MIME multipart email
local boundary="SecuBox_Report_$(date +%s)_$$"
local email_body="MIME-Version: 1.0
From: SecuBox Reporter <secubox@$hostname>
To: $recipient
Subject: $subject
Content-Type: multipart/alternative; boundary=\"$boundary\"
--$boundary
Content-Type: text/plain; charset=utf-8
SecuBox $report_name Report
===========================
Generated: $(date)
Hostname: $hostname
This report contains HTML content. Please view in an HTML-capable email client.
--$boundary
Content-Type: text/html; charset=utf-8
$html_content
--$boundary--"
# Try msmtp first, then sendmail
if command -v msmtp >/dev/null 2>&1 && [ -n "$smtp_server" ]; then
# Create temporary msmtp config
local msmtp_conf="/tmp/msmtp-report-$$.conf"
cat > "$msmtp_conf" << EOF
account default
host $smtp_server
port $smtp_port
auth on
user $smtp_user
password $smtp_password
tls $([ "$smtp_tls" = "1" ] && echo "on" || echo "off")
tls_starttls on
tls_certcheck off
from secubox@$hostname
EOF
chmod 600 "$msmtp_conf"
echo "$email_body" | msmtp -C "$msmtp_conf" "$recipient" 2>/dev/null
local result=$?
rm -f "$msmtp_conf"
return $result
elif command -v sendmail >/dev/null 2>&1; then
echo "$email_body" | sendmail -t 2>/dev/null
return $?
else
echo "ERROR: No mail transport available (msmtp or sendmail required)" >&2
return 1
fi
}
# Test email configuration
test_email() {
local recipient="$1"
[ -z "$recipient" ] && {
config_load secubox-reporter
config_get recipient email recipient ""
}
[ -z "$recipient" ] && {
echo "ERROR: No recipient configured" >&2
return 1
}
local hostname=$(uci -q get system.@system[0].hostname || hostname)
local test_body="<html><body><h1>SecuBox Email Test</h1><p>This is a test email from <strong>$hostname</strong>.</p><p>Generated: $(date)</p></body></html>"
if send_report_email "test" "$test_body" "$recipient"; then
echo "Test email sent to $recipient"
return 0
else
echo "Failed to send test email" >&2
return 1
fi
}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Development Status - {{HOSTNAME}}</title>
<style>
:root{--bg:#0a0a0f;--surface:#12121a;--card:#1a1a2e;--ink:#f0f2ff;--dim:rgba(240,242,255,.5);--muted:#666;--primary:#6366f1;--primary-end:#8b5cf6;--cyan:#06b6d4;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--glass:rgba(255,255,255,.04);--border:rgba(255,255,255,.08)}
*{margin:0;padding:0;box-sizing:border-box}
body{min-height:100vh;background:var(--bg);color:var(--ink);font-family:"Inter","Segoe UI",system-ui,sans-serif;padding:2rem;line-height:1.6}
.container{max-width:1200px;margin:0 auto}
h1{font-size:2rem;margin-bottom:.5rem;background:linear-gradient(90deg,var(--primary),var(--primary-end),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:2rem;padding:1.5rem;background:var(--card);border:1px solid var(--border);border-radius:12px}
.header-meta{display:flex;gap:1rem;flex-wrap:wrap}
.meta-item{font-size:.75rem;color:var(--muted);padding:.25rem .75rem;background:var(--glass);border-radius:4px}
.score-badge{font-size:2rem;font-weight:700;padding:1rem 1.5rem;background:linear-gradient(135deg,var(--primary),var(--primary-end));border-radius:12px;text-align:center}
.score-label{font-size:.6rem;text-transform:uppercase;letter-spacing:.1em;opacity:.7;display:block;margin-top:.25rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;margin-bottom:1.5rem}
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
.card-title{font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}
.card-title::before{content:"";width:3px;height:1em;background:var(--primary);border-radius:2px}
ul{list-style:none;padding-left:0}
li{padding:.5rem 0;border-bottom:1px solid var(--border);font-size:.9rem}
li:last-child{border-bottom:none}
li strong{color:var(--cyan)}
.history-entry{margin-bottom:1.5rem}
.history-date{font-size:.8rem;color:var(--primary);font-weight:600;margin-bottom:.5rem;padding:.25rem .5rem;background:rgba(99,102,241,.1);border-radius:4px;display:inline-block}
.wip-section{margin-bottom:1rem}
.wip-section h4{color:var(--cyan);font-size:.85rem;margin-bottom:.5rem}
.roadmap-version{display:flex;align-items:center;gap:1rem;padding:.75rem;background:var(--glass);border-radius:8px;margin-bottom:.5rem}
.roadmap-version h4{margin:0;flex:1}
.status-badge{font-size:.65rem;padding:.25rem .5rem;border-radius:4px;text-transform:uppercase;font-weight:600}
.status-badge.complete{background:rgba(34,197,94,.15);color:var(--green)}
.status-badge.inprogress{background:rgba(99,102,241,.15);color:var(--primary)}
.status-badge.pending{background:rgba(102,102,102,.15);color:var(--muted)}
.muted{color:var(--muted);font-style:italic}
footer{margin-top:3rem;text-align:center;color:var(--muted);font-size:.75rem;padding-top:1.5rem;border-top:1px solid var(--border)}
footer a{color:var(--primary)}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<h1>Development Status Report</h1>
<div class="header-meta">
<span class="meta-item">{{HOSTNAME}}</span>
<span class="meta-item">{{TIMESTAMP}}</span>
</div>
</div>
<div class="score-badge">
{{HEALTH_SCORE}}%
<span class="score-label">Health Score</span>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Completions</h3>
</div>
<div class="card-body">
{{HISTORY_ENTRIES}}
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Work In Progress</h3>
</div>
<div class="card-body">
{{WIP_ENTRIES}}
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Roadmap Progress</h3>
</div>
<div class="card-body">
{{ROADMAP_DATA}}
</div>
</div>
<footer>
Generated by SecuBox Report Generator v1.0
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{SUBJECT}}</title>
</head>
<body style="margin:0;padding:0;background:#0a0a0f;font-family:'Segoe UI',system-ui,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a0a0f;padding:20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background:#1a1a2e;border-radius:12px;overflow:hidden;">
<!-- Header -->
<tr>
<td style="background:linear-gradient(135deg,#6366f1,#8b5cf6);padding:30px;text-align:center;">
<h1 style="margin:0;color:#fff;font-size:24px;">SecuBox Report</h1>
<p style="margin:10px 0 0;color:rgba(255,255,255,0.8);font-size:14px;">{{HOSTNAME}} | {{TIMESTAMP}}</p>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding:30px;color:#f0f2ff;">
{{CONTENT}}
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding:20px;text-align:center;border-top:1px solid rgba(255,255,255,0.08);color:#666;font-size:12px;">
Generated by SecuBox Report Generator<br>
<a href="{{DASHBOARD_URL}}" style="color:#6366f1;text-decoration:none;">View Dashboard</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Services Status - {{HOSTNAME}}</title>
<style>
:root{--bg:#0a0a0f;--surface:#12121a;--card:#1a1a2e;--ink:#f0f2ff;--dim:rgba(240,242,255,.5);--muted:#666;--primary:#6366f1;--primary-end:#8b5cf6;--cyan:#06b6d4;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--glass:rgba(255,255,255,.04);--border:rgba(255,255,255,.08)}
*{margin:0;padding:0;box-sizing:border-box}
body{min-height:100vh;background:var(--bg);color:var(--ink);font-family:"Inter","Segoe UI",system-ui,sans-serif;padding:2rem;line-height:1.6}
.container{max-width:1200px;margin:0 auto}
h1{font-size:2rem;margin-bottom:.5rem;background:linear-gradient(90deg,var(--primary),var(--primary-end),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:2rem;padding:1.5rem;background:var(--card);border:1px solid var(--border);border-radius:12px}
.header-meta{display:flex;gap:1rem;flex-wrap:wrap}
.meta-item{font-size:.75rem;color:var(--muted);padding:.25rem .75rem;background:var(--glass);border-radius:4px}
.score-badge{font-size:2rem;font-weight:700;padding:1rem 1.5rem;background:linear-gradient(135deg,var(--primary),var(--primary-end));border-radius:12px;text-align:center}
.score-label{font-size:.6rem;text-transform:uppercase;letter-spacing:.1em;opacity:.7;display:block;margin-top:.25rem}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
.stat-badge{background:var(--card);border:1px solid var(--border);border-radius:8px;padding:1rem;text-align:center}
.stat-value{font-size:1.5rem;font-weight:700;background:linear-gradient(90deg,var(--primary),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.stat-label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:.25rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;margin-bottom:1.5rem}
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
.card-title{font-size:1rem;font-weight:600;display:flex;align-items:center;gap:.5rem}
.card-title::before{content:"";width:3px;height:1em;background:var(--primary);border-radius:2px}
.services-table{width:100%;border-collapse:collapse;font-size:.85rem}
.services-table th,.services-table td{padding:.75rem;text-align:left;border-bottom:1px solid var(--border)}
.services-table th{color:var(--dim);font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
.services-table a{color:var(--cyan);text-decoration:none}
.services-table a:hover{text-decoration:underline}
.services-table code{font-size:.75rem;padding:.15rem .35rem;background:var(--glass);border-radius:3px;font-family:"JetBrains Mono",monospace}
.status-badge{font-size:.65rem;padding:.25rem .5rem;border-radius:4px;text-transform:uppercase;font-weight:600}
.status-badge.running{background:rgba(34,197,94,.15);color:var(--green)}
.status-badge.stopped{background:rgba(239,68,68,.15);color:var(--red)}
.muted{color:var(--muted);font-style:italic}
footer{margin-top:3rem;text-align:center;color:var(--muted);font-size:.75rem;padding-top:1.5rem;border-top:1px solid var(--border)}
footer a{color:var(--primary)}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-info">
<h1>Distribution Status Report</h1>
<div class="header-meta">
<span class="meta-item">{{HOSTNAME}}</span>
<span class="meta-item">{{TIMESTAMP}}</span>
</div>
</div>
<div class="score-badge">
{{HEALTH_UP}}/{{HEALTH_TOTAL}}
<span class="score-label">Services Up</span>
</div>
</div>
<div class="stats-grid">
<div class="stat-badge">
<div class="stat-value">{{TOR_COUNT}}</div>
<div class="stat-label">Tor Services</div>
</div>
<div class="stat-badge">
<div class="stat-value">{{DNS_COUNT}}</div>
<div class="stat-label">DNS/SSL Vhosts</div>
</div>
<div class="stat-badge">
<div class="stat-value">{{MESH_COUNT}}</div>
<div class="stat-label">Mesh Services</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Tor Hidden Services</h3>
</div>
<div class="card-body">
{{TOR_SERVICES}}
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">DNS/SSL Services</h3>
</div>
<div class="card-body">
{{DNS_SERVICES}}
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Mesh Services</h3>
</div>
<div class="card-body">
{{MESH_SERVICES}}
</div>
</div>
<footer>
Generated by SecuBox Report Generator v1.0
</footer>
</div>
</body>
</html>