diff --git a/ai-insights-engine.sh b/ai-insights-engine.sh new file mode 100644 index 0000000..58a2ff7 --- /dev/null +++ b/ai-insights-engine.sh @@ -0,0 +1,672 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# █████╗ ██╗ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗████████╗███████╗ +# ██╔══██╗██║ ██║████╗ ██║██╔════╝██║██╔════╝ ██║ ██║╚══██╔══╝██╔════╝ +# ███████║██║ ██║██╔██╗ ██║███████╗██║██║ ███╗███████║ ██║ ███████╗ +# ██╔══██║██║ ██║██║╚██╗██║╚════██║██║██║ ██║██╔══██║ ██║ ╚════██║ +# ██║ ██║██║ ██║██║ ╚████║███████║██║╚██████╔╝██║ ██║ ██║ ███████║ +# ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD AI INSIGHTS ENGINE v3.0 +# Predictive Analytics, Anomaly Detection, Smart Recommendations +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +AI_HOME="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards}/ai" +AI_MODELS="$AI_HOME/models" +AI_TRAINING="$AI_HOME/training" +AI_PREDICTIONS="$AI_HOME/predictions" +mkdir -p "$AI_MODELS" "$AI_TRAINING" "$AI_PREDICTIONS" 2>/dev/null + +# Anomaly detection thresholds +ANOMALY_ZSCORE_THRESHOLD=2.5 +ANOMALY_PERCENTILE_THRESHOLD=95 + +# Prediction windows +PREDICTION_WINDOW_SHORT=5 # 5 minute prediction +PREDICTION_WINDOW_MEDIUM=30 # 30 minute prediction +PREDICTION_WINDOW_LONG=60 # 1 hour prediction + +# Historical data storage +declare -a METRIC_HISTORY_CPU=() +declare -a METRIC_HISTORY_MEM=() +declare -a METRIC_HISTORY_NET=() +declare -a METRIC_HISTORY_DISK=() +declare -a METRIC_TIMESTAMPS=() + +# Anomaly tracking +declare -A ANOMALY_SCORES +declare -A BASELINE_MEANS +declare -A BASELINE_STDDEVS + +#─────────────────────────────────────────────────────────────────────────────── +# STATISTICAL FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Calculate mean of array +calc_mean() { + local -a values=("$@") + local sum=0 + local count=${#values[@]} + + [[ $count -eq 0 ]] && echo "0" && return + + for v in "${values[@]}"; do + sum=$(echo "$sum + $v" | bc -l 2>/dev/null || echo "$((sum + v))") + done + + echo "scale=2; $sum / $count" | bc -l 2>/dev/null || echo "$((sum / count))" +} + +# Calculate standard deviation +calc_stddev() { + local -a values=("$@") + local mean=$(calc_mean "${values[@]}") + local sum_sq=0 + local count=${#values[@]} + + [[ $count -lt 2 ]] && echo "0" && return + + for v in "${values[@]}"; do + local diff=$(echo "$v - $mean" | bc -l 2>/dev/null || echo "$((v - mean))") + sum_sq=$(echo "$sum_sq + ($diff * $diff)" | bc -l 2>/dev/null || echo "$sum_sq") + done + + echo "scale=2; sqrt($sum_sq / ($count - 1))" | bc -l 2>/dev/null || echo "1" +} + +# Calculate Z-score +calc_zscore() { + local value="$1" + local mean="$2" + local stddev="$3" + + [[ "$stddev" == "0" ]] && echo "0" && return + + echo "scale=2; ($value - $mean) / $stddev" | bc -l 2>/dev/null || echo "0" +} + +# Calculate percentile +calc_percentile() { + local -a values=("$@") + local percentile="${values[-1]}" + unset 'values[-1]' + + local sorted=($(printf '%s\n' "${values[@]}" | sort -n)) + local count=${#sorted[@]} + local index=$(echo "scale=0; $count * $percentile / 100" | bc 2>/dev/null || echo "$((count * percentile / 100))") + + [[ $index -ge $count ]] && index=$((count - 1)) + echo "${sorted[$index]}" +} + +# Linear regression for trend prediction +linear_regression() { + local -a values=("$@") + local n=${#values[@]} + + [[ $n -lt 2 ]] && echo "0 0" && return + + local sum_x=0 sum_y=0 sum_xy=0 sum_xx=0 + + for ((i=0; i/dev/null || echo "$((sum_y + values[i]))") + sum_xy=$(echo "$sum_xy + ($i * ${values[$i]})" | bc -l 2>/dev/null) + sum_xx=$((sum_xx + i * i)) + done + + local slope=$(echo "scale=4; ($n * $sum_xy - $sum_x * $sum_y) / ($n * $sum_xx - $sum_x * $sum_x)" | bc -l 2>/dev/null || echo "0") + local intercept=$(echo "scale=4; ($sum_y - $slope * $sum_x) / $n" | bc -l 2>/dev/null || echo "0") + + echo "$slope $intercept" +} + +# Exponential moving average +calc_ema() { + local -a values=("$@") + local alpha="${values[-1]}" + unset 'values[-1]' + + local ema="${values[0]}" + + for ((i=1; i<${#values[@]}; i++)); do + ema=$(echo "scale=4; $alpha * ${values[$i]} + (1 - $alpha) * $ema" | bc -l 2>/dev/null) + done + + echo "$ema" +} + +#─────────────────────────────────────────────────────────────────────────────── +# ANOMALY DETECTION +#─────────────────────────────────────────────────────────────────────────────── + +# Detect anomalies using Z-score method +detect_zscore_anomaly() { + local metric_name="$1" + local current_value="$2" + local -a history=("${@:3}") + + local mean=$(calc_mean "${history[@]}") + local stddev=$(calc_stddev "${history[@]}") + local zscore=$(calc_zscore "$current_value" "$mean" "$stddev") + + BASELINE_MEANS[$metric_name]="$mean" + BASELINE_STDDEVS[$metric_name]="$stddev" + + local abs_zscore=$(echo "$zscore" | tr -d '-') + + if (( $(echo "$abs_zscore > $ANOMALY_ZSCORE_THRESHOLD" | bc -l 2>/dev/null || echo 0) )); then + echo "ANOMALY|$zscore|$mean|$stddev" + return 1 + fi + + echo "NORMAL|$zscore|$mean|$stddev" + return 0 +} + +# Detect anomalies using IQR method +detect_iqr_anomaly() { + local current_value="$1" + shift + local -a history=("$@") + + local q1=$(calc_percentile "${history[@]}" 25) + local q3=$(calc_percentile "${history[@]}" 75) + local iqr=$(echo "$q3 - $q1" | bc -l 2>/dev/null || echo "$((q3 - q1))") + + local lower=$(echo "$q1 - 1.5 * $iqr" | bc -l 2>/dev/null) + local upper=$(echo "$q3 + 1.5 * $iqr" | bc -l 2>/dev/null) + + if (( $(echo "$current_value < $lower || $current_value > $upper" | bc -l 2>/dev/null || echo 0) )); then + echo "ANOMALY|$lower|$upper" + return 1 + fi + + echo "NORMAL|$lower|$upper" + return 0 +} + +# Combined anomaly detection with confidence score +detect_anomaly() { + local metric_name="$1" + local current_value="$2" + shift 2 + local -a history=("$@") + + [[ ${#history[@]} -lt 10 ]] && echo "INSUFFICIENT_DATA|0" && return 0 + + local zscore_result=$(detect_zscore_anomaly "$metric_name" "$current_value" "${history[@]}") + local iqr_result=$(detect_iqr_anomaly "$current_value" "${history[@]}") + + local zscore_status=$(echo "$zscore_result" | cut -d'|' -f1) + local iqr_status=$(echo "$iqr_result" | cut -d'|' -f1) + + local confidence=0 + local status="NORMAL" + + if [[ "$zscore_status" == "ANOMALY" ]]; then + confidence=$((confidence + 50)) + status="ANOMALY" + fi + + if [[ "$iqr_status" == "ANOMALY" ]]; then + confidence=$((confidence + 50)) + status="ANOMALY" + fi + + ANOMALY_SCORES[$metric_name]="$confidence" + + echo "$status|$confidence|$zscore_result" +} + +#─────────────────────────────────────────────────────────────────────────────── +# PREDICTIVE ANALYTICS +#─────────────────────────────────────────────────────────────────────────────── + +# Predict future value using linear regression +predict_linear() { + local -a history=("$@") + local steps_ahead="${history[-1]}" + unset 'history[-1]' + + read slope intercept <<< "$(linear_regression "${history[@]}")" + + local n=${#history[@]} + local prediction=$(echo "scale=2; $slope * ($n + $steps_ahead) + $intercept" | bc -l 2>/dev/null) + + echo "$prediction" +} + +# Predict using exponential smoothing +predict_exponential() { + local -a history=("$@") + local steps_ahead="${history[-1]}" + local alpha="${history[-2]}" + unset 'history[-1]' 'history[-2]' + + local ema=$(calc_ema "${history[@]}" "$alpha") + + # Simple exponential prediction + echo "$ema" +} + +# Predict with confidence interval +predict_with_confidence() { + local metric_name="$1" + shift + local -a history=("$@") + local steps_ahead="${history[-1]}" + unset 'history[-1]' + + [[ ${#history[@]} -lt 5 ]] && echo "0|0|0" && return + + local prediction=$(predict_linear "${history[@]}" "$steps_ahead") + local stddev=$(calc_stddev "${history[@]}") + + local lower=$(echo "scale=2; $prediction - 1.96 * $stddev" | bc -l 2>/dev/null || echo "$prediction") + local upper=$(echo "scale=2; $prediction + 1.96 * $stddev" | bc -l 2>/dev/null || echo "$prediction") + + echo "$prediction|$lower|$upper" +} + +# Trend analysis +analyze_trend() { + local -a history=("$@") + + [[ ${#history[@]} -lt 3 ]] && echo "UNKNOWN|0" && return + + read slope intercept <<< "$(linear_regression "${history[@]}")" + + local trend="STABLE" + local strength=0 + + if (( $(echo "$slope > 0.5" | bc -l 2>/dev/null || echo 0) )); then + trend="RISING" + strength=$(echo "scale=0; $slope * 10" | bc 2>/dev/null || echo "5") + elif (( $(echo "$slope < -0.5" | bc -l 2>/dev/null || echo 0) )); then + trend="FALLING" + strength=$(echo "scale=0; $slope * -10" | bc 2>/dev/null || echo "5") + fi + + echo "$trend|$strength" +} + +#─────────────────────────────────────────────────────────────────────────────── +# PATTERN RECOGNITION +#─────────────────────────────────────────────────────────────────────────────── + +# Detect cyclic patterns (daily, hourly) +detect_cycles() { + local -a history=("$@") + local len=${#history[@]} + + [[ $len -lt 24 ]] && echo "INSUFFICIENT_DATA" && return + + # Check for hourly pattern (correlation with 1-hour lag) + local hourly_corr=0 + if [[ $len -ge 60 ]]; then + # Simple autocorrelation check + local matches=0 + for ((i=60; i/dev/null || echo "1") + if (( $(echo "$ratio > $threshold" | bc -l 2>/dev/null || echo 0) )); then + spikes+=("$i:${history[$i]}") + fi + done + + echo "${spikes[*]}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# SMART RECOMMENDATIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Generate recommendations based on metrics +generate_recommendations() { + local -a recommendations=() + + # CPU recommendations + local cpu_mean="${BASELINE_MEANS[cpu]:-50}" + local cpu_trend=$(analyze_trend "${METRIC_HISTORY_CPU[@]}") + + if (( $(echo "$cpu_mean > 80" | bc -l 2>/dev/null || echo 0) )); then + recommendations+=("CRITICAL|CPU consistently high ($cpu_mean%). Consider scaling or optimizing processes.") + elif [[ "$cpu_trend" == RISING* ]]; then + recommendations+=("WARNING|CPU usage trending upward. Monitor for potential capacity issues.") + fi + + # Memory recommendations + local mem_mean="${BASELINE_MEANS[memory]:-50}" + + if (( $(echo "$mem_mean > 85" | bc -l 2>/dev/null || echo 0) )); then + recommendations+=("CRITICAL|Memory usage critical ($mem_mean%). Risk of OOM. Add more RAM or optimize.") + elif (( $(echo "$mem_mean > 70" | bc -l 2>/dev/null || echo 0) )); then + recommendations+=("WARNING|Memory usage elevated ($mem_mean%). Consider memory optimization.") + fi + + # Disk recommendations + local disk_usage=$(get_disk_usage "/" 2>/dev/null || echo "50") + + if [[ $disk_usage -gt 90 ]]; then + recommendations+=("CRITICAL|Disk space critically low ($disk_usage%). Immediate cleanup required.") + elif [[ $disk_usage -gt 80 ]]; then + recommendations+=("WARNING|Disk space getting low ($disk_usage%). Plan for cleanup or expansion.") + fi + + # Anomaly-based recommendations + for metric in "${!ANOMALY_SCORES[@]}"; do + local score="${ANOMALY_SCORES[$metric]}" + if [[ $score -gt 75 ]]; then + recommendations+=("ALERT|Anomaly detected in $metric (confidence: $score%). Investigate unusual behavior.") + fi + done + + printf '%s\n' "${recommendations[@]}" +} + +# Prioritized action items +generate_action_items() { + local -a actions=() + + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + + # Priority 1: Critical issues + [[ $mem -gt 95 ]] && actions+=("P1|IMMEDIATE|Kill non-essential processes to free memory") + [[ $disk -gt 95 ]] && actions+=("P1|IMMEDIATE|Clear disk space - system at risk") + + # Priority 2: High issues + [[ $cpu -gt 90 ]] && actions+=("P2|URGENT|Investigate high CPU processes") + [[ $mem -gt 85 ]] && actions+=("P2|URGENT|Review memory-hungry applications") + [[ $disk -gt 85 ]] && actions+=("P2|URGENT|Plan disk cleanup or expansion") + + # Priority 3: Medium issues + [[ $cpu -gt 70 ]] && actions+=("P3|MONITOR|CPU elevated - watch for increases") + [[ $mem -gt 70 ]] && actions+=("P3|MONITOR|Memory usage above optimal") + + # Priority 4: Optimization suggestions + [[ $cpu -lt 20 && $mem -lt 30 ]] && actions+=("P4|OPTIMIZE|Resources underutilized - consider downsizing") + + printf '%s\n' "${actions[@]}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DATA COLLECTION +#─────────────────────────────────────────────────────────────────────────────── + +# Collect current metrics +collect_metrics() { + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + local timestamp=$(date +%s) + + METRIC_HISTORY_CPU+=("$cpu") + METRIC_HISTORY_MEM+=("$mem") + METRIC_HISTORY_DISK+=("$disk") + METRIC_TIMESTAMPS+=("$timestamp") + + # Trim to last 1000 entries + local max=1000 + [[ ${#METRIC_HISTORY_CPU[@]} -gt $max ]] && METRIC_HISTORY_CPU=("${METRIC_HISTORY_CPU[@]:(-$max)}") + [[ ${#METRIC_HISTORY_MEM[@]} -gt $max ]] && METRIC_HISTORY_MEM=("${METRIC_HISTORY_MEM[@]:(-$max)}") + [[ ${#METRIC_HISTORY_DISK[@]} -gt $max ]] && METRIC_HISTORY_DISK=("${METRIC_HISTORY_DISK[@]:(-$max)}") + [[ ${#METRIC_TIMESTAMPS[@]} -gt $max ]] && METRIC_TIMESTAMPS=("${METRIC_TIMESTAMPS[@]:(-$max)}") + + echo "$timestamp|$cpu|$mem|$disk" +} + +# Load historical data +load_history() { + local history_file="$AI_TRAINING/metrics_history.dat" + + [[ ! -f "$history_file" ]] && return + + while IFS='|' read -r ts cpu mem disk; do + METRIC_HISTORY_CPU+=("$cpu") + METRIC_HISTORY_MEM+=("$mem") + METRIC_HISTORY_DISK+=("$disk") + METRIC_TIMESTAMPS+=("$ts") + done < "$history_file" +} + +# Save history +save_history() { + local history_file="$AI_TRAINING/metrics_history.dat" + local max=10000 + + { + for ((i=0; i<${#METRIC_TIMESTAMPS[@]} && i "$history_file" +} + +#─────────────────────────────────────────────────────────────────────────────── +# AI INSIGHTS DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_ai_dashboard() { + clear_screen + cursor_hide + + # Animated header + local colors=("$BR_CYAN" "$BR_PURPLE" "$BR_PINK" "$BR_ORANGE") + local frame=$(($(date +%s) % 4)) + + printf "${colors[$frame]}${BOLD}" + cat << 'HEADER' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ▄▄▄ ██▓ ██▓ ███▄ █ ██████ ██▓ ▄████ ██░ ██ ▄▄▄█████▓ ║ +║ ▒████▄ ▓██▒ ▓██▒ ██ ▀█ █ ▒██ ▒ ▓██▒ ██▒ ▀█▒▓██░ ██▒▓ ██▒ ▓▒ ║ +║ ▒██ ▀█▄ ▒██▒ ▒██▒▓██ ▀█ ██▒░ ▓██▄ ▒██▒▒██░▄▄▄░▒██▀▀██░▒ ▓██░ ▒░ ║ +║ ░██▄▄▄▄██ ░██░ ░██░▓██▒ ▐▌██▒ ▒ ██▒░██░░▓█ ██▓░▓█ ░██ ░ ▓██▓ ░ ║ +║ ▓█ ▓██▒░██░ ░██░▒██░ ▓██░▒██████▒▒░██░░▒▓███▀▒░▓█▒░██▓ ▒██▒ ░ ║ +║ ▒▒ ▓▒█░░▓ ░▓ ░ ▒░ ▒ ▒ ▒ ▒▓▒ ▒ ░░▓ ░▒ ▒ ▒ ░░▒░▒ ▒ ░░ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +HEADER + printf "${RST}\n" + + # Collect fresh data + collect_metrics > /dev/null + + # Current metrics with predictions + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + + printf "${BR_CYAN}┌─ REAL-TIME METRICS ──────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_CYAN}│${RST}\n" + + # CPU with prediction + local cpu_pred=$(predict_with_confidence "cpu" "${METRIC_HISTORY_CPU[@]}" 5) + local cpu_trend=$(analyze_trend "${METRIC_HISTORY_CPU[@]}") + IFS='|' read -r cpu_forecast cpu_low cpu_high <<< "$cpu_pred" + IFS='|' read -r cpu_direction cpu_strength <<< "$cpu_trend" + + local cpu_color="$BR_GREEN" + [[ $cpu -gt 70 ]] && cpu_color="$BR_YELLOW" + [[ $cpu -gt 85 ]] && cpu_color="$BR_RED" + + printf "${BR_CYAN}│${RST} ${BOLD}CPU${RST} Current: ${cpu_color}%3d%%${RST} │ Forecast: ${TEXT_SECONDARY}%s%%${RST} │ Trend: " + case "$cpu_direction" in + RISING) printf "${BR_RED}↗ Rising${RST}" ;; + FALLING) printf "${BR_GREEN}↘ Falling${RST}" ;; + *) printf "${TEXT_MUTED}→ Stable${RST}" ;; + esac + printf "\n" "$cpu" "$cpu_forecast" + + # Memory with prediction + local mem_pred=$(predict_with_confidence "memory" "${METRIC_HISTORY_MEM[@]}" 5) + local mem_trend=$(analyze_trend "${METRIC_HISTORY_MEM[@]}") + IFS='|' read -r mem_forecast mem_low mem_high <<< "$mem_pred" + IFS='|' read -r mem_direction mem_strength <<< "$mem_trend" + + local mem_color="$BR_GREEN" + [[ $mem -gt 70 ]] && mem_color="$BR_YELLOW" + [[ $mem -gt 85 ]] && mem_color="$BR_RED" + + printf "${BR_CYAN}│${RST} ${BOLD}Memory${RST} Current: ${mem_color}%3d%%${RST} │ Forecast: ${TEXT_SECONDARY}%s%%${RST} │ Trend: " + case "$mem_direction" in + RISING) printf "${BR_RED}↗ Rising${RST}" ;; + FALLING) printf "${BR_GREEN}↘ Falling${RST}" ;; + *) printf "${TEXT_MUTED}→ Stable${RST}" ;; + esac + printf "\n" "$mem" "$mem_forecast" + + # Disk + local disk_color="$BR_GREEN" + [[ $disk -gt 80 ]] && disk_color="$BR_YELLOW" + [[ $disk -gt 90 ]] && disk_color="$BR_RED" + + printf "${BR_CYAN}│${RST} ${BOLD}Disk${RST} Current: ${disk_color}%3d%%${RST} │\n" "$disk" + printf "${BR_CYAN}│${RST}\n" + printf "${BR_CYAN}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # Anomaly Detection Panel + printf "${BR_PURPLE}┌─ ANOMALY DETECTION ──────────────────────────────────────────────────────┐${RST}\n" + + if [[ ${#METRIC_HISTORY_CPU[@]} -ge 10 ]]; then + local cpu_anomaly=$(detect_anomaly "cpu" "$cpu" "${METRIC_HISTORY_CPU[@]}") + local mem_anomaly=$(detect_anomaly "memory" "$mem" "${METRIC_HISTORY_MEM[@]}") + + IFS='|' read -r cpu_status cpu_conf _ <<< "$cpu_anomaly" + IFS='|' read -r mem_status mem_conf _ <<< "$mem_anomaly" + + if [[ "$cpu_status" == "ANOMALY" ]]; then + printf "${BR_PURPLE}│${RST} ${BR_RED}⚠ CPU ANOMALY${RST} detected (confidence: %s%%) ${BR_RED}█${RST}\n" "$cpu_conf" + else + printf "${BR_PURPLE}│${RST} ${BR_GREEN}✓ CPU Normal${RST} (within expected range)\n" + fi + + if [[ "$mem_status" == "ANOMALY" ]]; then + printf "${BR_PURPLE}│${RST} ${BR_RED}⚠ MEMORY ANOMALY${RST} detected (confidence: %s%%) ${BR_RED}█${RST}\n" "$mem_conf" + else + printf "${BR_PURPLE}│${RST} ${BR_GREEN}✓ Memory Normal${RST} (within expected range)\n" + fi + else + printf "${BR_PURPLE}│${RST} ${TEXT_MUTED}Collecting baseline data... (%d/10 samples)${RST}\n" "${#METRIC_HISTORY_CPU[@]}" + fi + + printf "${BR_PURPLE}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # AI Recommendations + printf "${BR_ORANGE}┌─ AI RECOMMENDATIONS ─────────────────────────────────────────────────────┐${RST}\n" + + local -a recs + mapfile -t recs < <(generate_recommendations) + + if [[ ${#recs[@]} -eq 0 ]]; then + printf "${BR_ORANGE}│${RST} ${BR_GREEN}✓${RST} All systems operating within normal parameters\n" + else + for rec in "${recs[@]}"; do + IFS='|' read -r severity message <<< "$rec" + case "$severity" in + CRITICAL) printf "${BR_ORANGE}│${RST} ${BR_RED}🚨 %s${RST}\n" "$message" ;; + WARNING) printf "${BR_ORANGE}│${RST} ${BR_YELLOW}⚠️ %s${RST}\n" "$message" ;; + ALERT) printf "${BR_ORANGE}│${RST} ${BR_PURPLE}🔔 %s${RST}\n" "$message" ;; + *) printf "${BR_ORANGE}│${RST} ${BR_CYAN}ℹ️ %s${RST}\n" "$message" ;; + esac + done + fi + + printf "${BR_ORANGE}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # Action Items + printf "${BR_PINK}┌─ PRIORITY ACTIONS ───────────────────────────────────────────────────────┐${RST}\n" + + local -a actions + mapfile -t actions < <(generate_action_items) + + if [[ ${#actions[@]} -eq 0 ]]; then + printf "${BR_PINK}│${RST} ${BR_GREEN}No immediate actions required${RST}\n" + else + for action in "${actions[@]:0:5}"; do # Show top 5 + IFS='|' read -r priority urgency message <<< "$action" + case "$priority" in + P1) printf "${BR_PINK}│${RST} ${BR_RED}[P1] ${BOLD}%s${RST}: %s\n" "$urgency" "$message" ;; + P2) printf "${BR_PINK}│${RST} ${BR_ORANGE}[P2] %s${RST}: %s\n" "$urgency" "$message" ;; + P3) printf "${BR_PINK}│${RST} ${BR_YELLOW}[P3] %s${RST}: %s\n" "$urgency" "$message" ;; + *) printf "${BR_PINK}│${RST} ${BR_CYAN}[P4] %s${RST}: %s\n" "$urgency" "$message" ;; + esac + done + fi + + printf "${BR_PINK}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + printf "${TEXT_MUTED}Data points: %d │ Last updated: %s │ [r]efresh [s]ave [q]uit${RST}\n" \ + "${#METRIC_HISTORY_CPU[@]}" "$(date '+%H:%M:%S')" +} + +# Main loop +ai_dashboard_loop() { + load_history + + while true; do + render_ai_dashboard + + if read -rsn1 -t 3 key 2>/dev/null; then + case "$key" in + r|R) continue ;; + s|S) save_history; printf "\n${BR_GREEN}History saved.${RST}"; sleep 1 ;; + q|Q) save_history; break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) ai_dashboard_loop ;; + predict) predict_with_confidence "$2" "${@:3}" ;; + anomaly) detect_anomaly "$2" "$3" "${@:4}" ;; + trend) analyze_trend "${@:2}" ;; + *) + printf "Usage: %s [dashboard|predict|anomaly|trend]\n" "$0" + ;; + esac +fi diff --git a/analytics-dashboard.sh b/analytics-dashboard.sh new file mode 100644 index 0000000..16652cf --- /dev/null +++ b/analytics-dashboard.sh @@ -0,0 +1,548 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# █████╗ ███╗ ██╗ █████╗ ██╗ ██╗ ██╗ ████████╗ ██╗ ██████╗ ███████╗ +# ██╔══██╗ ████╗ ██║██╔══██╗ ██║ ╚██╗ ██╔╝ ╚══██╔══╝ ██║ ██╔════╝ ██╔════╝ +# ███████║ ██╔██╗ ██║███████║ ██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ +# ██╔══██║ ██║╚██╗██║██╔══██║ ██║ ╚██╔╝ ██║ ██║ ██║ ╚════██║ +# ██║ ██║ ██║ ╚████║██║ ██║ ███████╗██║ ██║ ██║ ╚██████╗ ███████║ +# ╚═╝ ╚═╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD ANALYTICS DASHBOARD v2.0 +# Advanced metrics visualization, trends, and insights +#═══════════════════════════════════════════════════════════════════════════════ + +# Source libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" +[[ -f "$SCRIPT_DIR/lib-cache.sh" ]] && source "$SCRIPT_DIR/lib-cache.sh" +[[ -f "$SCRIPT_DIR/lib-parallel.sh" ]] && source "$SCRIPT_DIR/lib-parallel.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +ANALYTICS_HOME="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards}/analytics" +METRICS_DB="$ANALYTICS_HOME/metrics.db" +mkdir -p "$ANALYTICS_HOME" 2>/dev/null + +# Refresh interval (seconds) +REFRESH_INTERVAL=5 + +# Historical data points to keep +MAX_HISTORY_POINTS=100 + +# Metrics history arrays +declare -a CPU_HISTORY=() +declare -a MEM_HISTORY=() +declare -a NET_HISTORY=() +declare -a DISK_HISTORY=() + +#─────────────────────────────────────────────────────────────────────────────── +# CHART RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +# Sparkline with color gradient +render_sparkline() { + local -a values=("$@") + local chars="▁▂▃▄▅▆▇█" + local max=1 min=0 + + # Find min/max + for v in "${values[@]}"; do + ((v > max)) && max=$v + done + + local range=$((max - min)) + [[ $range -eq 0 ]] && range=1 + + for v in "${values[@]}"; do + local idx=$(( (v - min) * 7 / range )) + # Color based on value (green->yellow->red) + local color + if ((v < 50)); then + color="$BR_GREEN" + elif ((v < 80)); then + color="$BR_YELLOW" + else + color="$BR_RED" + fi + printf "${color}${chars:$idx:1}${RST}" + done +} + +# Bar chart (horizontal) +render_bar_chart() { + local -a labels=() + local -a values=() + local max_value=0 + local bar_width=40 + + # Parse input (label:value format) + while [[ $# -gt 0 ]]; do + local item="$1" + local label=$(echo "$item" | cut -d: -f1) + local value=$(echo "$item" | cut -d: -f2) + + labels+=("$label") + values+=("$value") + ((value > max_value)) && max_value=$value + shift + done + + [[ $max_value -eq 0 ]] && max_value=1 + + for i in "${!labels[@]}"; do + local bar_len=$((values[i] * bar_width / max_value)) + local color="$BR_CYAN" + + ((values[i] > 75)) && color="$BR_RED" + ((values[i] > 50 && values[i] <= 75)) && color="$BR_YELLOW" + + printf " ${TEXT_SECONDARY}%-12s${RST} " "${labels[$i]}" + printf "${color}" + printf "%0.s█" $(seq 1 "$bar_len" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $((bar_width - bar_len)) 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%%${RST}\n" "${values[$i]}" + done +} + +# Pie chart (ASCII approximation) +render_pie_chart() { + local -a labels=() + local -a values=() + local total=0 + + # Parse input + while [[ $# -gt 0 ]]; do + labels+=("$(echo "$1" | cut -d: -f1)") + values+=("$(echo "$1" | cut -d: -f2)") + total=$((total + $(echo "$1" | cut -d: -f2))) + shift + done + + [[ $total -eq 0 ]] && total=1 + + local colors=("$BR_CYAN" "$BR_PINK" "$BR_ORANGE" "$BR_PURPLE" "$BR_GREEN" "$BR_YELLOW") + local pie_chars="○◔◑◕●" + + printf " " + for i in "${!labels[@]}"; do + local pct=$((values[i] * 100 / total)) + local fill_idx=$((pct * 4 / 100)) + [[ $fill_idx -gt 4 ]] && fill_idx=4 + + printf "${colors[$((i % 6))]}${pie_chars:$fill_idx:1}${RST} " + done + printf "\n\n" + + # Legend + for i in "${!labels[@]}"; do + local pct=$((values[i] * 100 / total)) + printf " ${colors[$((i % 6))]}●${RST} %-15s %3d%% (%d)\n" "${labels[$i]}" "$pct" "${values[$i]}" + done +} + +# Timeline chart +render_timeline() { + local -a timestamps=() + local -a values=() + local width=60 + local height=8 + + # Parse input + while [[ $# -gt 0 ]]; do + values+=("$1") + shift + done + + local max=1 + for v in "${values[@]}"; do + ((v > max)) && max=$v + done + + # Create grid + local -a grid=() + for ((row=0; row= threshold)); then + grid[$row]+="█" + else + grid[$row]+=" " + fi + done + done + + # Render + for ((row=0; row/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + + # Add to history + CPU_HISTORY+=("$cpu") + MEM_HISTORY+=("$mem") + DISK_HISTORY+=("$disk") + + # Trim history + [[ ${#CPU_HISTORY[@]} -gt $MAX_HISTORY_POINTS ]] && CPU_HISTORY=("${CPU_HISTORY[@]:1}") + [[ ${#MEM_HISTORY[@]} -gt $MAX_HISTORY_POINTS ]] && MEM_HISTORY=("${MEM_HISTORY[@]:1}") + [[ ${#DISK_HISTORY[@]} -gt $MAX_HISTORY_POINTS ]] && DISK_HISTORY=("${DISK_HISTORY[@]:1}") +} + +# Calculate statistics +calc_stats() { + local -a values=("$@") + local sum=0 count=${#values[@]} min=${values[0]:-0} max=${values[0]:-0} + + for v in "${values[@]}"; do + sum=$((sum + v)) + ((v < min)) && min=$v + ((v > max)) && max=$v + done + + local avg=$((count > 0 ? sum / count : 0)) + + echo "$min $max $avg" +} + +# Trend detection +detect_trend() { + local -a values=("$@") + local len=${#values[@]} + + [[ $len -lt 3 ]] && echo "stable" && return + + local first_half_avg=0 second_half_avg=0 + local mid=$((len / 2)) + + for ((i=0; i 5)); then + echo "rising" + elif ((diff < -5)); then + echo "falling" + else + echo "stable" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# DASHBOARD PANELS +#─────────────────────────────────────────────────────────────────────────────── + +# System overview panel +panel_system_overview() { + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + local uptime_sec=$(get_uptime_seconds 2>/dev/null || echo "0") + local uptime_str=$(time_ago "$uptime_sec" 2>/dev/null || echo "unknown") + + printf "${BR_CYAN}${BOLD}┌─ SYSTEM OVERVIEW ─────────────────────────────────────────┐${RST}\n" + printf "${BR_CYAN}│${RST}\n" + + render_bar_chart "CPU:$cpu" "Memory:$mem" "Disk:$disk" + + printf "${BR_CYAN}│${RST}\n" + printf "${BR_CYAN}│${RST} ${TEXT_SECONDARY}Hostname:${RST} %-20s ${TEXT_SECONDARY}Uptime:${RST} %s\n" "$(hostname)" "$uptime_str" + printf "${BR_CYAN}│${RST} ${TEXT_SECONDARY}OS:${RST} %-25s ${TEXT_SECONDARY}Kernel:${RST} %s\n" "$(uname -s)" "$(uname -r)" + printf "${BR_CYAN}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +# Real-time metrics panel +panel_realtime_metrics() { + printf "${BR_PURPLE}${BOLD}┌─ REAL-TIME METRICS ────────────────────────────────────────┐${RST}\n" + printf "${BR_PURPLE}│${RST}\n" + + printf "${BR_PURPLE}│${RST} ${TEXT_SECONDARY}CPU History:${RST} " + render_sparkline "${CPU_HISTORY[@]}" + printf "\n" + + printf "${BR_PURPLE}│${RST} ${TEXT_SECONDARY}Memory History:${RST} " + render_sparkline "${MEM_HISTORY[@]}" + printf "\n" + + printf "${BR_PURPLE}│${RST} ${TEXT_SECONDARY}Disk History:${RST} " + render_sparkline "${DISK_HISTORY[@]}" + printf "\n" + + # Stats + if [[ ${#CPU_HISTORY[@]} -gt 0 ]]; then + read min max avg <<< "$(calc_stats "${CPU_HISTORY[@]}")" + local trend=$(detect_trend "${CPU_HISTORY[@]}") + local trend_icon="→" + [[ "$trend" == "rising" ]] && trend_icon="↗" + [[ "$trend" == "falling" ]] && trend_icon="↘" + + printf "${BR_PURPLE}│${RST}\n" + printf "${BR_PURPLE}│${RST} ${TEXT_MUTED}CPU: min=%d%% max=%d%% avg=%d%% trend=%s${RST}\n" "$min" "$max" "$avg" "$trend_icon" + fi + + printf "${BR_PURPLE}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +# Network status panel +panel_network_status() { + local devices=( + "Lucidia Prime:192.168.4.38" + "BlackRoad Pi:192.168.4.64" + "Lucidia Alt:192.168.4.99" + "iPhone Koder:192.168.4.68" + "Codex VPS:159.65.43.12" + ) + + printf "${BR_GREEN}${BOLD}┌─ NETWORK STATUS ───────────────────────────────────────────┐${RST}\n" + + for device in "${devices[@]}"; do + local name=$(echo "$device" | cut -d: -f1) + local host=$(echo "$device" | cut -d: -f2) + + if ping -c 1 -W 1 "$host" &>/dev/null; then + printf "${BR_GREEN}│${RST} ${BR_GREEN}◉${RST} %-18s ${TEXT_MUTED}%s${RST}\n" "$name" "$host" + else + printf "${BR_GREEN}│${RST} ${BR_RED}○${RST} %-18s ${TEXT_MUTED}%s${RST}\n" "$name" "$host" + fi + done + + printf "${BR_GREEN}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +# API health panel +panel_api_health() { + printf "${BR_ORANGE}${BOLD}┌─ API HEALTH ───────────────────────────────────────────────┐${RST}\n" + + local apis=( + "GitHub:https://api.github.com" + "Cloudflare:https://api.cloudflare.com" + "CoinGecko:https://api.coingecko.com" + "Railway:https://backboard.railway.app" + ) + + for api in "${apis[@]}"; do + local name=$(echo "$api" | cut -d: -f1) + local url=$(echo "$api" | cut -d: -f2-) + + local status_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "$url" 2>/dev/null) + + if [[ "$status_code" == "200" ]] || [[ "$status_code" == "301" ]] || [[ "$status_code" == "302" ]]; then + printf "${BR_ORANGE}│${RST} ${BR_GREEN}✓${RST} %-12s ${TEXT_MUTED}%s${RST}\n" "$name" "$status_code" + else + printf "${BR_ORANGE}│${RST} ${BR_RED}✗${RST} %-12s ${TEXT_MUTED}%s${RST}\n" "$name" "${status_code:-timeout}" + fi + done + + printf "${BR_ORANGE}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +# Crypto dashboard panel +panel_crypto() { + printf "${BR_YELLOW}${BOLD}┌─ CRYPTO PORTFOLIO ─────────────────────────────────────────┐${RST}\n" + + # Fetch prices + local btc_data=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local eth_data=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local sol_data=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + + if command -v jq &>/dev/null; then + local btc_price=$(echo "$btc_data" | jq -r '.bitcoin.usd // 0') + local btc_change=$(echo "$btc_data" | jq -r '.bitcoin.usd_24h_change // 0' | cut -c1-5) + local eth_price=$(echo "$eth_data" | jq -r '.ethereum.usd // 0') + local eth_change=$(echo "$eth_data" | jq -r '.ethereum.usd_24h_change // 0' | cut -c1-5) + local sol_price=$(echo "$sol_data" | jq -r '.solana.usd // 0') + local sol_change=$(echo "$sol_data" | jq -r '.solana.usd_24h_change // 0' | cut -c1-5) + + local btc_color="$BR_GREEN" + local eth_color="$BR_GREEN" + local sol_color="$BR_GREEN" + + [[ "${btc_change:0:1}" == "-" ]] && btc_color="$BR_RED" + [[ "${eth_change:0:1}" == "-" ]] && eth_color="$BR_RED" + [[ "${sol_change:0:1}" == "-" ]] && sol_color="$BR_RED" + + printf "${BR_YELLOW}│${RST} ${BR_ORANGE}₿${RST} Bitcoin ${TEXT_PRIMARY}\$%-10s${RST} ${btc_color}%s%%${RST}\n" "$btc_price" "$btc_change" + printf "${BR_YELLOW}│${RST} ${BR_PURPLE}Ξ${RST} Ethereum ${TEXT_PRIMARY}\$%-10s${RST} ${eth_color}%s%%${RST}\n" "$eth_price" "$eth_change" + printf "${BR_YELLOW}│${RST} ${BR_CYAN}◎${RST} Solana ${TEXT_PRIMARY}\$%-10s${RST} ${sol_color}%s%%${RST}\n" "$sol_price" "$sol_change" + else + printf "${BR_YELLOW}│${RST} ${TEXT_MUTED}Install jq for price display${RST}\n" + fi + + printf "${BR_YELLOW}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +# Insights panel +panel_insights() { + printf "${BR_PINK}${BOLD}┌─ AI INSIGHTS ──────────────────────────────────────────────┐${RST}\n" + + # Generate insights based on metrics + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + + if ((cpu > 80)); then + printf "${BR_PINK}│${RST} ${BR_RED}⚠${RST} High CPU usage detected (%d%%) - consider scaling\n" "$cpu" + elif ((cpu < 10)); then + printf "${BR_PINK}│${RST} ${BR_GREEN}✓${RST} CPU resources underutilized - room for more workloads\n" + else + printf "${BR_PINK}│${RST} ${BR_CYAN}ℹ${RST} CPU usage normal (%d%%)\n" "$cpu" + fi + + if ((mem > 85)); then + printf "${BR_PINK}│${RST} ${BR_RED}⚠${RST} Memory pressure high (%d%%) - optimize or upgrade\n" "$mem" + else + printf "${BR_PINK}│${RST} ${BR_CYAN}ℹ${RST} Memory usage healthy (%d%%)\n" "$mem" + fi + + # Check trends + if [[ ${#CPU_HISTORY[@]} -ge 5 ]]; then + local cpu_trend=$(detect_trend "${CPU_HISTORY[@]}") + case "$cpu_trend" in + rising) + printf "${BR_PINK}│${RST} ${BR_YELLOW}↗${RST} CPU usage trending upward\n" + ;; + falling) + printf "${BR_PINK}│${RST} ${BR_GREEN}↘${RST} CPU usage trending downward\n" + ;; + esac + fi + + printf "${BR_PINK}│${RST}\n" + printf "${BR_PINK}│${RST} ${TEXT_MUTED}Last updated: $(date '+%H:%M:%S')${RST}\n" + printf "${BR_PINK}└────────────────────────────────────────────────────────────┘${RST}\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_dashboard() { + clear_screen + get_term_size + + # Header + printf "${BOLD}" + printf "${BR_CYAN}╔══════════════════════════════════════════════════════════════════════════════╗${RST}\n" + printf "${BR_CYAN}║${RST}" + printf "${BR_ORANGE} █████╗ ███╗ ██╗ █████╗ ██╗ ██╗ ██╗████████╗██╗ ██████╗███████╗${RST}" + printf "${BR_CYAN}║${RST}\n" + printf "${BR_CYAN}║${RST}" + printf "${BR_PINK} ██╔══██╗████╗ ██║██╔══██╗██║ ╚██╗ ██╔╝╚══██╔══╝██║██╔════╝██╔════╝${RST}" + printf "${BR_CYAN}║${RST}\n" + printf "${BR_CYAN}║${RST}" + printf "${BR_PURPLE} ███████║██╔██╗ ██║███████║██║ ╚████╔╝ ██║ ██║██║ ███████╗${RST}" + printf "${BR_CYAN}║${RST}\n" + printf "${BR_CYAN}║${RST}" + printf "${BR_CYAN} ██╔══██║██║╚██╗██║██╔══██║██║ ╚██╔╝ ██║ ██║██║ ╚════██║${RST}" + printf "${BR_CYAN}║${RST}\n" + printf "${BR_CYAN}║${RST}" + printf "${BR_GREEN} ██║ ██║██║ ╚████║██║ ██║███████╗██║ ██║ ██║╚██████╗███████║${RST}" + printf "${BR_CYAN}║${RST}\n" + printf "${BR_CYAN}╚══════════════════════════════════════════════════════════════════════════════╝${RST}\n" + printf "\n" + + # Collect current metrics + collect_metrics + + # Render panels + panel_system_overview + printf "\n" + panel_realtime_metrics + printf "\n" + + # Two column layout for smaller panels + panel_network_status & + local net_pid=$! + panel_api_health & + local api_pid=$! + wait $net_pid $api_pid + + printf "\n" + panel_crypto + printf "\n" + panel_insights + + # Footer + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf "${TEXT_SECONDARY}[r] Refresh [e] Export [t] Theme [q] Quit${RST}\n" +} + +main_loop() { + cursor_hide + stty -echo 2>/dev/null + + local last_refresh=0 + + while true; do + local now=$(date +%s) + + # Auto-refresh + if ((now - last_refresh >= REFRESH_INTERVAL)); then + render_dashboard + last_refresh=$now + fi + + # Check for input + if read -rsn1 -t 1 key 2>/dev/null; then + case "$key" in + r|R) render_dashboard; last_refresh=$(date +%s) ;; + e|E) + source "$SCRIPT_DIR/data-export.sh" 2>/dev/null + export_ui + ;; + t|T) + source "$SCRIPT_DIR/themes-premium.sh" 2>/dev/null + theme_selector + ;; + q|Q) break ;; + esac + fi + done + + cursor_show + stty echo 2>/dev/null + printf "\n${TEXT_MUTED}Analytics dashboard closed.${RST}\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-run}" in + run) main_loop ;; + once) render_dashboard ;; + metrics) collect_system_metrics ;; + *) + printf "Usage: %s [run|once|metrics]\n" "$0" + printf " %s run # Interactive dashboard\n" "$0" + printf " %s once # Single render\n" "$0" + ;; + esac +fi diff --git a/ascii-art.sh b/ascii-art.sh new file mode 100644 index 0000000..3d39fc8 --- /dev/null +++ b/ascii-art.sh @@ -0,0 +1,1085 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# █████╗ ███████╗ ██████╗██╗██╗ █████╗ ██████╗ ████████╗ +# ██╔══██╗██╔════╝██╔════╝██║██║ ██╔══██╗██╔══██╗╚══██╔══╝ +# ███████║███████╗██║ ██║██║ ███████║██████╔╝ ██║ +# ██╔══██║╚════██║██║ ██║██║ ██╔══██║██╔══██╗ ██║ +# ██║ ██║███████║╚██████╗██║██║ ██║ ██║██║ ██║ ██║ +# ╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD ASCII ART GENERATOR v3.0 +# Text to ASCII, Image to ASCII, Banners & FIGlet Compatible +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# BUILT-IN ASCII FONTS +#─────────────────────────────────────────────────────────────────────────────── + +# Block font (uppercase only) +declare -A FONT_BLOCK +FONT_BLOCK[A]=' + ███╗ +██╔██╗ +█████║ +██╔██║ +██║██║' +FONT_BLOCK[B]=' +████╗ +██╔██╗ +████╔╝ +██╔██╗ +████╔╝' +FONT_BLOCK[C]=' + ████╗ +██╔══╝ +██║ +██║ +╚████╗' +FONT_BLOCK[D]=' +████╗ +██╔██╗ +██║██║ +██║██║ +████╔╝' +FONT_BLOCK[E]=' +█████╗ +██╔══╝ +████╗ +██╔══╝ +█████╗' +FONT_BLOCK[F]=' +█████╗ +██╔══╝ +████╗ +██╔══╝ +██║ ' +FONT_BLOCK[G]=' + ████╗ +██╔══╝ +██║██╗ +██║██║ +╚████║' +FONT_BLOCK[H]=' +██╗██╗ +██║██║ +█████║ +██╔██║ +██║██║' +FONT_BLOCK[I]=' +████╗ +╚██╔╝ + ██║ + ██║ +████╗' +FONT_BLOCK[J]=' + ██╗ + ██║ + ██║ +██╗██║ +╚███╔╝' +FONT_BLOCK[K]=' +██╗██╗ +██║██╔ +███╔╝ +██╔██╗ +██║██║' +FONT_BLOCK[L]=' +██╗ +██║ +██║ +██║ +█████╗' +FONT_BLOCK[M]=' +██╗ ██╗ +███╗██║ +██╔███║ +██║╚██║ +██║ ██║' +FONT_BLOCK[N]=' +██╗ ██╗ +███╗██║ +█╔███║ +██║╚██║ +██║ ██║' +FONT_BLOCK[O]=' + ███╗ +██╔██╗ +██║██║ +██║██║ +╚███╔╝' +FONT_BLOCK[P]=' +████╗ +██╔██╗ +████╔╝ +██╔══╝ +██║ ' +FONT_BLOCK[Q]=' + ███╗ +██╔██╗ +██║██║ +██╔██║ +╚██╔██╗' +FONT_BLOCK[R]=' +████╗ +██╔██╗ +████╔╝ +██╔██╗ +██║██║' +FONT_BLOCK[S]=' +█████╗ +██╔══╝ +╚███╗ + ╚═██║ +████╔╝' +FONT_BLOCK[T]=' +█████╗ +╚═██═╝ + ██║ + ██║ + ██║ ' +FONT_BLOCK[U]=' +██╗██╗ +██║██║ +██║██║ +██║██║ +╚███╔╝' +FONT_BLOCK[V]=' +██╗██╗ +██║██║ +██║██║ +╚███╔╝ + ╚═╝ ' +FONT_BLOCK[W]=' +██╗ ██╗ +██║ ██║ +██║ ██║ +██╔███║ +╚═╝╚═╝ ' +FONT_BLOCK[X]=' +██╗██╗ +╚███╔╝ + ███╗ +██╔██╗ +██╝██║' +FONT_BLOCK[Y]=' +██╗██╗ +╚███╔╝ + ██║ + ██║ + ██║ ' +FONT_BLOCK[Z]=' +████╗ +╚═██║ + ██╔╝ +██╔╝ +████╗' +FONT_BLOCK[0]=' + ███╗ +██╗██║ +██║██║ +██║██║ +╚███╔╝' +FONT_BLOCK[1]=' + ██╗ +███║ +╚██║ + ██║ +███╗' +FONT_BLOCK[2]=' +████╗ +╚══██ + ██╔╝ +██╔╝ +████╗' +FONT_BLOCK[3]=' +████╗ +╚══██ + ██╔╝ +╚═██║ +███╔╝' +FONT_BLOCK[4]=' +██╗██ +██║██ +████╗ +╚═██║ + ██║' +FONT_BLOCK[5]=' +████╗ +██══╝ +███╗ +╚═██║ +███╔╝' +FONT_BLOCK[6]=' + ███╗ +██══╝ +███╗ +██╔██ +╚██╔╝' +FONT_BLOCK[7]=' +████╗ +╚═██║ + ██║ + ██║ + ██║' +FONT_BLOCK[8]=' + ███╗ +██╔██ +╚██╔╝ +██╔██ +╚██╔╝' +FONT_BLOCK[9]=' + ███╗ +██╔██ +╚███║ +╚═██║ +███╔╝' +FONT_BLOCK[' ']=' + + + + + ' +FONT_BLOCK['!']=' +██╗ +██║ +██║ +╚═╝ +██╗' + +#─────────────────────────────────────────────────────────────────────────────── +# SIMPLE BANNER FONT +#─────────────────────────────────────────────────────────────────────────────── + +declare -A FONT_SIMPLE +FONT_SIMPLE[A]=' + A + A A +AAAAA +A A +A A' +FONT_SIMPLE[B]=' +BBBB +B B +BBBB +B B +BBBB ' +FONT_SIMPLE[C]=' + CCC +C +C +C + CCC ' +FONT_SIMPLE[D]=' +DDD +D D +D D +D D +DDD ' +FONT_SIMPLE[E]=' +EEEEE +E +EEE +E +EEEEE' +FONT_SIMPLE[F]=' +FFFFF +F +FFF +F +F ' +FONT_SIMPLE[G]=' + GGG +G +G GG +G G + GGG ' +FONT_SIMPLE[H]=' +H H +H H +HHHHH +H H +H H' +FONT_SIMPLE[I]=' +IIIII + I + I + I +IIIII' +FONT_SIMPLE[J]=' +JJJJJ + J + J +J J + JJJ ' +FONT_SIMPLE[K]=' +K K +K K +KKK +K K +K K' +FONT_SIMPLE[L]=' +L +L +L +L +LLLLL' +FONT_SIMPLE[M]=' +M M +MM MM +M M M +M M +M M' +FONT_SIMPLE[N]=' +N N +NN N +N N N +N NN +N N' +FONT_SIMPLE[O]=' + OOO +O O +O O +O O + OOO ' +FONT_SIMPLE[P]=' +PPPP +P P +PPPP +P +P ' +FONT_SIMPLE[Q]=' + QQQ +Q Q +Q Q +Q Q + QQ Q' +FONT_SIMPLE[R]=' +RRRR +R R +RRRR +R R +R R' +FONT_SIMPLE[S]=' + SSS +S + SSS + S +SSSS ' +FONT_SIMPLE[T]=' +TTTTT + T + T + T + T ' +FONT_SIMPLE[U]=' +U U +U U +U U +U U + UUU ' +FONT_SIMPLE[V]=' +V V +V V +V V + V V + V ' +FONT_SIMPLE[W]=' +W W +W W +W W W +WW WW +W W' +FONT_SIMPLE[X]=' +X X + X X + X + X X +X X' +FONT_SIMPLE[Y]=' +Y Y + Y Y + Y + Y + Y ' +FONT_SIMPLE[Z]=' +ZZZZZ + Z + Z + Z +ZZZZZ' +FONT_SIMPLE[' ']=' + + + + + ' + +#─────────────────────────────────────────────────────────────────────────────── +# SHADOW FONT +#─────────────────────────────────────────────────────────────────────────────── + +declare -A FONT_SHADOW +FONT_SHADOW[A]=' + _ + / \ + / _ \ + / ___ \ +/_/ \_\' +FONT_SHADOW[B]=' + ____ +| __ ) +| _ \ +| |_) | +|____/ ' +FONT_SHADOW[C]=' + ____ + / ___| +| | +| |___ + \____|' +FONT_SHADOW[D]=' + ____ +| _ \ +| | | | +| |_| | +|____/ ' +FONT_SHADOW[E]=' + _____ +| ____| +| _| +| |___ +|_____|' +FONT_SHADOW[F]=' + _____ +| ___| +| |_ +| _| +|_| ' +FONT_SHADOW[G]=' + ____ + / ___| +| | _ +| |_| | + \____|' +FONT_SHADOW[H]=' + _ _ +| | | | +| |_| | +| _ | +|_| |_|' +FONT_SHADOW[I]=' + ___ +|_ _| + | | + | | +|___|' +FONT_SHADOW[J]=' + _ + | | + _ | | +| |_| | + \___/ ' +FONT_SHADOW[K]=' + _ __ +| |/ / +| | / +| |\ \ +|_| \_\' +FONT_SHADOW[L]=' + _ +| | +| | +| |___ +|_____|' +FONT_SHADOW[M]=' + __ __ +| \/ | +| |\/| | +| | | | +|_| |_|' +FONT_SHADOW[N]=' + _ _ +| \ | | +| \| | +| |\ | +|_| \_|' +FONT_SHADOW[O]=' + ___ + / _ \ +| | | | +| |_| | + \___/ ' +FONT_SHADOW[P]=' + ____ +| _ \ +| |_) | +| __/ +|_| ' +FONT_SHADOW[Q]=' + ___ + / _ \ +| | | | +| |_| | + \__\_\' +FONT_SHADOW[R]=' + ____ +| _ \ +| |_) | +| _ < +|_| \_\' +FONT_SHADOW[S]=' + ____ +/ ___| +\___ \ + ___) | +|____/ ' +FONT_SHADOW[T]=' + _____ +|_ _| + | | + | | + |_| ' +FONT_SHADOW[U]=' + _ _ +| | | | +| | | | +| |_| | + \___/ ' +FONT_SHADOW[V]=' +__ __ +\ \ / / + \ \ / / + \ V / + \_/ ' +FONT_SHADOW[W]=' +__ __ +\ \ / / + \ \ /\ / / + \ V V / + \_/\_/ ' +FONT_SHADOW[X]=' +__ __ +\ \/ / + \ / + / \ +/_/\_\' +FONT_SHADOW[Y]=' +__ __ +\ \ / / + \ V / + | | + |_| ' +FONT_SHADOW[Z]=' + _____ +|__ / + / / + / /_ +/____|' +FONT_SHADOW[' ']=' + + + + + ' + +#─────────────────────────────────────────────────────────────────────────────── +# TEXT TO ASCII +#─────────────────────────────────────────────────────────────────────────────── + +# Get number of lines in a font character +get_font_height() { + local font_name="${1:-SIMPLE}" + local -n font_ref="FONT_${font_name^^}" + + local sample="${font_ref[A]}" + local height=0 + + while IFS= read -r line; do + ((height++)) + done <<< "$sample" + + echo "$height" +} + +# Render text in ASCII art +render_ascii_text() { + local text="$1" + local font_name="${2:-SIMPLE}" + local color="${3:-}" + + local -n font_ref="FONT_${font_name^^}" + local height=$(get_font_height "$font_name") + + text="${text^^}" # Convert to uppercase + + # Build output line by line + for ((line=1; line<=height; line++)); do + for ((i=0; i<${#text}; i++)); do + local char="${text:$i:1}" + local char_art="${font_ref[$char]:-${font_ref[' ']}}" + + # Get the specific line + local char_line=$(echo "$char_art" | sed -n "${line}p") + [[ -n "$color" ]] && printf "%s" "$color" + printf "%s" "$char_line" + [[ -n "$color" ]] && printf "${RST}" + done + printf "\n" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# ASCII ART BOXES +#─────────────────────────────────────────────────────────────────────────────── + +render_box() { + local text="$1" + local style="${2:-single}" + local padding="${3:-1}" + local color="${4:-}" + + local text_len=${#text} + local inner_width=$((text_len + padding * 2)) + + # Box characters + local tl tr bl br h v + case "$style" in + single) + tl="┌" tr="┐" bl="└" br="┘" h="─" v="│" + ;; + double) + tl="╔" tr="╗" bl="╚" br="╝" h="═" v="║" + ;; + bold) + tl="┏" tr="┓" bl="┗" br="┛" h="━" v="┃" + ;; + round) + tl="╭" tr="╮" bl="╰" br="╯" h="─" v="│" + ;; + ascii) + tl="+" tr="+" bl="+" br="+" h="-" v="|" + ;; + *) + tl="┌" tr="┐" bl="└" br="┘" h="─" v="│" + ;; + esac + + [[ -n "$color" ]] && printf "%s" "$color" + + # Top border + printf "%s" "$tl" + printf "%0.s$h" $(seq 1 $inner_width) + printf "%s\n" "$tr" + + # Content + printf "%s" "$v" + printf "%*s%s%*s" "$padding" "" "$text" "$padding" "" + printf "%s\n" "$v" + + # Bottom border + printf "%s" "$bl" + printf "%0.s$h" $(seq 1 $inner_width) + printf "%s\n" "$br" + + [[ -n "$color" ]] && printf "${RST}" +} + +render_multiline_box() { + local style="${1:-single}" + local color="${2:-}" + shift 2 + local -a lines=("$@") + + # Find max line length + local max_len=0 + for line in "${lines[@]}"; do + [[ ${#line} -gt $max_len ]] && max_len=${#line} + done + + local inner_width=$((max_len + 2)) + + # Box characters + local tl tr bl br h v + case "$style" in + single) tl="┌" tr="┐" bl="└" br="┘" h="─" v="│" ;; + double) tl="╔" tr="╗" bl="╚" br="╝" h="═" v="║" ;; + bold) tl="┏" tr="┓" bl="┗" br="┛" h="━" v="┃" ;; + round) tl="╭" tr="╮" bl="╰" br="╯" h="─" v="│" ;; + *) tl="┌" tr="┐" bl="└" br="┘" h="─" v="│" ;; + esac + + [[ -n "$color" ]] && printf "%s" "$color" + + # Top border + printf "%s" "$tl" + printf "%0.s$h" $(seq 1 $inner_width) + printf "%s\n" "$tr" + + # Content lines + for line in "${lines[@]}"; do + printf "%s %-*s %s\n" "$v" "$max_len" "$line" "$v" + done + + # Bottom border + printf "%s" "$bl" + printf "%0.s$h" $(seq 1 $inner_width) + printf "%s\n" "$br" + + [[ -n "$color" ]] && printf "${RST}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# ASCII SHAPES +#─────────────────────────────────────────────────────────────────────────────── + +render_triangle() { + local height="${1:-5}" + local char="${2:-*}" + local color="${3:-}" + + [[ -n "$color" ]] && printf "%s" "$color" + + for ((i=1; i<=height; i++)); do + local spaces=$((height - i)) + local chars=$((2 * i - 1)) + + printf "%*s" "$spaces" "" + printf "%0.s$char" $(seq 1 $chars) + printf "\n" + done + + [[ -n "$color" ]] && printf "${RST}" +} + +render_diamond() { + local size="${1:-5}" + local char="${2:-*}" + local color="${3:-}" + + [[ -n "$color" ]] && printf "%s" "$color" + + # Top half + for ((i=1; i<=size; i++)); do + printf "%*s" "$((size - i))" "" + printf "%0.s$char" $(seq 1 $((2 * i - 1))) + printf "\n" + done + + # Bottom half + for ((i=size-1; i>=1; i--)); do + printf "%*s" "$((size - i))" "" + printf "%0.s$char" $(seq 1 $((2 * i - 1))) + printf "\n" + done + + [[ -n "$color" ]] && printf "${RST}" +} + +render_heart() { + local color="${1:-$BR_RED}" + + printf "%s" "$color" + cat << 'EOF' + **** **** + ******** ******** +********************* + ******************* + *************** + *********** + ******* + *** + * +EOF + printf "${RST}" +} + +render_star() { + local color="${1:-$BR_YELLOW}" + + printf "%s" "$color" + cat << 'EOF' + * + *** + ***** + ************* + *********** + ********* + *** *** + ** ** + * * +EOF + printf "${RST}" +} + +render_skull() { + local color="${1:-}" + + [[ -n "$color" ]] && printf "%s" "$color" + cat << 'EOF' + ______ + .-. .-. + ( | | ) + \ \ / / + ) ) ( ( + / / () \ \ + ( / || \ ) + \\ /__\ // + \\______// + |______| + / || || \ + / || || \ + /___||__||___\ +EOF + [[ -n "$color" ]] && printf "${RST}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# IMAGE TO ASCII +#─────────────────────────────────────────────────────────────────────────────── + +image_to_ascii() { + local image_path="$1" + local width="${2:-80}" + local chars="${3:- .:-=+*#%@}" + + if [[ ! -f "$image_path" ]]; then + printf "${BR_RED}Image not found: %s${RST}\n" "$image_path" + return 1 + fi + + # Check for required tools + if ! command -v convert &>/dev/null; then + printf "${TEXT_MUTED}ImageMagick required for image conversion${RST}\n" + + # Try jp2a if available + if command -v jp2a &>/dev/null; then + jp2a --width="$width" "$image_path" + return + fi + + return 1 + fi + + # Convert image to grayscale and resize + local temp_file="/tmp/ascii_temp_$$.txt" + + convert "$image_path" \ + -resize "${width}x" \ + -colorspace Gray \ + -depth 8 \ + txt:- 2>/dev/null | \ + tail -n +2 | \ + while IFS=: read -r coord color; do + # Extract grayscale value + local gray=$(echo "$color" | grep -oP 'gray\(\K[0-9]+') + [[ -z "$gray" ]] && gray=0 + + # Map to character + local char_count=${#chars} + local idx=$((gray * (char_count - 1) / 255)) + printf "%s" "${chars:$idx:1}" + + # Newline at end of row + local x=$(echo "$coord" | grep -oP '^\d+') + local next_x=$((x + 1)) + if [[ "$coord" == *",0:" ]]; then + [[ "$x" != "0" ]] && printf "\n" + fi + done + + printf "\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# GRADIENT TEXT +#─────────────────────────────────────────────────────────────────────────────── + +render_gradient_text() { + local text="$1" + local start_r="${2:-255}" + local start_g="${3:-0}" + local start_b="${4:-0}" + local end_r="${5:-0}" + local end_g="${6:-0}" + local end_b="${7:-255}" + + local len=${#text} + + for ((i=0; i/dev/null || echo 0) + + local r=$(printf "%.0f" "$(echo "$start_r + ($end_r - $start_r) * $ratio" | bc -l 2>/dev/null || echo $start_r)") + local g=$(printf "%.0f" "$(echo "$start_g + ($end_g - $start_g) * $ratio" | bc -l 2>/dev/null || echo $start_g)") + local b=$(printf "%.0f" "$(echo "$start_b + ($end_b - $start_b) * $ratio" | bc -l 2>/dev/null || echo $start_b)") + + printf "\033[38;2;%d;%d;%dm%s" "$r" "$g" "$b" "${text:$i:1}" + done + printf "${RST}\n" +} + +render_rainbow_text() { + local text="$1" + local len=${#text} + + local -a colors=( + "255;0;0" # Red + "255;127;0" # Orange + "255;255;0" # Yellow + "0;255;0" # Green + "0;255;255" # Cyan + "0;0;255" # Blue + "127;0;255" # Purple + ) + + for ((i=0; i [font] Convert text to ASCII (fonts: SIMPLE, BLOCK, SHADOW)\n" + printf " banner Generate boxed banner\n" + printf " box [style] Draw text in box (single, double, bold, round)\n" + printf " rainbow Rainbow colored text\n" + printf " gradient Gradient colored text\n" + printf " heart/star/skull Render shape\n" + printf " triangle/diamond Render shape with size\n" + printf " image [width] Convert image to ASCII\n" + ;; + esac +fi diff --git a/calendar-manager.sh b/calendar-manager.sh new file mode 100644 index 0000000..d737067 --- /dev/null +++ b/calendar-manager.sh @@ -0,0 +1,680 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ █████╗ ██╗ ███████╗███╗ ██╗██████╗ █████╗ ██████╗ +# ██╔════╝██╔══██╗██║ ██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔══██╗ +# ██║ ███████║██║ █████╗ ██╔██╗ ██║██║ ██║███████║██████╔╝ +# ██║ ██╔══██║██║ ██╔══╝ ██║╚██╗██║██║ ██║██╔══██║██╔══██╗ +# ╚██████╗██║ ██║███████╗███████╗██║ ╚████║██████╔╝██║ ██║██║ ██║ +# ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD CALENDAR & TASK MANAGER v1.0 +# Terminal Calendar with Events & Todo Lists +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/blackroad-calendar" +EVENTS_FILE="$DATA_DIR/events.json" +TASKS_FILE="$DATA_DIR/tasks.json" + +CURRENT_YEAR=$(date +%Y) +CURRENT_MONTH=$(date +%m) +CURRENT_DAY=$(date +%d) + +SELECTED_YEAR=$CURRENT_YEAR +SELECTED_MONTH=$CURRENT_MONTH +SELECTED_DAY=$CURRENT_DAY + +MONTH_NAMES=("" "January" "February" "March" "April" "May" "June" + "July" "August" "September" "October" "November" "December") + +DAY_NAMES=("Su" "Mo" "Tu" "We" "Th" "Fr" "Sa") + +#─────────────────────────────────────────────────────────────────────────────── +# DATA MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +init_data() { + mkdir -p "$DATA_DIR" + [[ ! -f "$EVENTS_FILE" ]] && echo '{"events":[]}' > "$EVENTS_FILE" + [[ ! -f "$TASKS_FILE" ]] && echo '{"tasks":[]}' > "$TASKS_FILE" +} + +get_events() { + local date="$1" + if command -v jq &>/dev/null && [[ -f "$EVENTS_FILE" ]]; then + jq -r ".events[] | select(.date == \"$date\") | .title" "$EVENTS_FILE" 2>/dev/null + fi +} + +get_all_events() { + if command -v jq &>/dev/null && [[ -f "$EVENTS_FILE" ]]; then + jq -r '.events[] | "\(.date)|\(.title)|\(.time // "")|\(.color // "39")"' "$EVENTS_FILE" 2>/dev/null + fi +} + +add_event() { + local date="$1" + local title="$2" + local time="${3:-}" + local color="${4:-39}" + + if command -v jq &>/dev/null; then + local new_event=$(jq -n \ + --arg date "$date" \ + --arg title "$title" \ + --arg time "$time" \ + --arg color "$color" \ + '{date: $date, title: $title, time: $time, color: $color}') + + jq ".events += [$new_event]" "$EVENTS_FILE" > "$EVENTS_FILE.tmp" && \ + mv "$EVENTS_FILE.tmp" "$EVENTS_FILE" + fi +} + +delete_event() { + local date="$1" + local title="$2" + + if command -v jq &>/dev/null; then + jq "del(.events[] | select(.date == \"$date\" and .title == \"$title\"))" "$EVENTS_FILE" > "$EVENTS_FILE.tmp" && \ + mv "$EVENTS_FILE.tmp" "$EVENTS_FILE" + fi +} + +get_tasks() { + local filter="${1:-all}" + + if command -v jq &>/dev/null && [[ -f "$TASKS_FILE" ]]; then + case "$filter" in + pending) + jq -r '.tasks[] | select(.done == false) | "\(.id)|\(.title)|\(.priority)|\(.due // "")"' "$TASKS_FILE" 2>/dev/null + ;; + done) + jq -r '.tasks[] | select(.done == true) | "\(.id)|\(.title)|\(.priority)|\(.due // "")"' "$TASKS_FILE" 2>/dev/null + ;; + *) + jq -r '.tasks[] | "\(.id)|\(.title)|\(.priority)|\(.due // "")|\(.done)"' "$TASKS_FILE" 2>/dev/null + ;; + esac + fi +} + +add_task() { + local title="$1" + local priority="${2:-2}" + local due="${3:-}" + + if command -v jq &>/dev/null; then + local id=$(date +%s%N | sha256sum | head -c 8) + + local new_task=$(jq -n \ + --arg id "$id" \ + --arg title "$title" \ + --argjson priority "$priority" \ + --arg due "$due" \ + '{id: $id, title: $title, priority: $priority, due: $due, done: false, created: now | todate}') + + jq ".tasks += [$new_task]" "$TASKS_FILE" > "$TASKS_FILE.tmp" && \ + mv "$TASKS_FILE.tmp" "$TASKS_FILE" + fi +} + +toggle_task() { + local id="$1" + + if command -v jq &>/dev/null; then + jq "(.tasks[] | select(.id == \"$id\") | .done) |= not" "$TASKS_FILE" > "$TASKS_FILE.tmp" && \ + mv "$TASKS_FILE.tmp" "$TASKS_FILE" + fi +} + +delete_task() { + local id="$1" + + if command -v jq &>/dev/null; then + jq "del(.tasks[] | select(.id == \"$id\"))" "$TASKS_FILE" > "$TASKS_FILE.tmp" && \ + mv "$TASKS_FILE.tmp" "$TASKS_FILE" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# CALENDAR FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +get_days_in_month() { + local year="$1" + local month="$2" + + case "$month" in + 1|3|5|7|8|10|12) echo 31 ;; + 4|6|9|11) echo 30 ;; + 2) + if [[ $((year % 4)) -eq 0 && ($((year % 100)) -ne 0 || $((year % 400)) -eq 0) ]]; then + echo 29 + else + echo 28 + fi + ;; + esac +} + +get_first_weekday() { + local year="$1" + local month="$2" + + date -d "$year-$month-01" +%w 2>/dev/null || \ + date -j -f "%Y-%m-%d" "$year-$month-01" +%w 2>/dev/null || echo 0 +} + +has_events() { + local date="$1" + local events=$(get_events "$date") + [[ -n "$events" ]] +} + +#─────────────────────────────────────────────────────────────────────────────── +# RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +render_header() { + printf "\033[1;38;5;214m" + cat << 'EOF' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 📅 CALENDAR & TASK MANAGER ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +EOF + printf "\033[0m" +} + +render_calendar() { + local year="$SELECTED_YEAR" + local month="$SELECTED_MONTH" + local month_num=$((10#$month)) + + local days_in_month=$(get_days_in_month "$year" "$month_num") + local first_weekday=$(get_first_weekday "$year" "$(printf '%02d' $month_num)") + + printf "\n \033[1;38;5;51m◀\033[0m \033[1;38;5;226m%s %d\033[0m \033[1;38;5;51m▶\033[0m\n\n" "${MONTH_NAMES[$month_num]}" "$year" + + # Day headers + printf " " + for day_name in "${DAY_NAMES[@]}"; do + printf "\033[38;5;39m%4s\033[0m" "$day_name" + done + printf "\n" + + # Days + printf " " + for ((i=0; i [time]\n" + exit 1 + fi + + add_event "$date" "$title" "$time" + printf "✓ Event added: %s on %s\n" "$title" "$date" +} + +cmd_list_tasks() { + local filter="${1:-pending}" + + printf "\n \033[1;38;5;46mTasks (%s)\033[0m\n" "$filter" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + while IFS='|' read -r id title priority due; do + [[ -z "$id" ]] && continue + + local priority_icon="●" + case "$priority" in + 1) priority_icon="\033[38;5;196m!!!\033[0m" ;; + 2) priority_icon="\033[38;5;226m!!\033[0m" ;; + 3) priority_icon="\033[38;5;46m!\033[0m" ;; + esac + + printf " [%s] %s %s" "$id" "$priority_icon" "$title" + [[ -n "$due" ]] && printf " (due: %s)" "$due" + printf "\n" + done < <(get_tasks "$filter") +} + +cmd_add_task() { + local title="$1" + local priority="${2:-2}" + local due="${3:-}" + + if [[ -z "$title" ]]; then + printf "Usage: calendar-manager.sh task-add <title> [priority] [due]\n" + exit 1 + fi + + add_task "$title" "$priority" "$due" + printf "✓ Task added: %s\n" "$title" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'EOF' + + BLACKROAD CALENDAR & TASK MANAGER + ══════════════════════════════════ + + Usage: calendar-manager.sh [command] [options] + + Commands: + (none) Interactive calendar mode + list [date] List events for date (default: today) + add <date> <title> Add event + tasks [filter] List tasks (pending/done/all) + task-add <title> Add task + + Interactive Controls: + ←→ Previous/Next day + ↑↓ Previous/Next week + n/p Next/Previous month + y/u Next/Previous year + g Go to today + a Add event + t Add task + d Delete event + q Quit + + Examples: + calendar-manager.sh # Interactive mode + calendar-manager.sh list 2024-01-15 # Events for date + calendar-manager.sh add 2024-01-20 "Meeting" + calendar-manager.sh tasks pending + calendar-manager.sh task-add "Review code" 1 2024-01-18 + +EOF +} + +main() { + init_data + + if [[ $# -eq 0 ]]; then + interactive_mode + exit 0 + fi + + case "$1" in + list) + cmd_list_events "$2" + ;; + add) + cmd_add_event "$2" "$3" "$4" + ;; + tasks) + cmd_list_tasks "$2" + ;; + task-add) + cmd_add_task "$2" "$3" "$4" + ;; + -h|--help|help) + show_help + ;; + *) + show_help + exit 1 + ;; + esac +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/cicd-monitor.sh b/cicd-monitor.sh new file mode 100644 index 0000000..2924490 --- /dev/null +++ b/cicd-monitor.sh @@ -0,0 +1,715 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗██╗ ██╗ ██████╗██████╗ ███╗ ███╗ ██████╗ ███╗ ██╗██╗████████╗ ██████╗ ██████╗ +# ██╔════╝██║ ██║██╔════╝██╔══██╗ ████╗ ████║██╔═══██╗████╗ ██║██║╚══██╔══╝██╔═══██╗██╔══██╗ +# ██║ ██║ ██║██║ ██║ ██║ ██╔████╔██║██║ ██║██╔██╗ ██║██║ ██║ ██║ ██║██████╔╝ +# ██║ ██║ ██║██║ ██║ ██║ ██║╚██╔╝██║██║ ██║██║╚██╗██║██║ ██║ ██║ ██║██╔══██╗ +# ╚██████╗██║ ██║╚██████╗██████╔╝ ██║ ╚═╝ ██║╚██████╔╝██║ ╚████║██║ ██║ ╚██████╔╝██║ ██║ +# ╚═════╝╚═╝ ╚═╝ ╚═════╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD CI/CD PIPELINE MONITOR v3.0 +# GitHub Actions, GitLab CI, Jenkins & CircleCI Dashboard +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +CACHE_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/cache/cicd" +CONFIG_FILE="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/cicd_config.json" + +mkdir -p "$CACHE_DIR" 2>/dev/null + +# API Tokens (from environment or config) +GITHUB_TOKEN="${GITHUB_TOKEN:-}" +GITLAB_TOKEN="${GITLAB_TOKEN:-}" +JENKINS_TOKEN="${JENKINS_TOKEN:-}" +CIRCLECI_TOKEN="${CIRCLECI_TOKEN:-}" + +#─────────────────────────────────────────────────────────────────────────────── +# GITHUB ACTIONS +#─────────────────────────────────────────────────────────────────────────────── + +gh_get_workflows() { + local repo="$1" + + if command -v gh &>/dev/null; then + gh api "repos/$repo/actions/workflows" 2>/dev/null + elif [[ -n "$GITHUB_TOKEN" ]]; then + curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$repo/actions/workflows" 2>/dev/null + else + echo '{"workflows":[]}' + fi +} + +gh_get_runs() { + local repo="$1" + local workflow_id="${2:-}" + local per_page="${3:-10}" + + local url="repos/$repo/actions/runs?per_page=$per_page" + [[ -n "$workflow_id" ]] && url+="&workflow_id=$workflow_id" + + if command -v gh &>/dev/null; then + gh api "$url" 2>/dev/null + elif [[ -n "$GITHUB_TOKEN" ]]; then + curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/$url" 2>/dev/null + else + echo '{"workflow_runs":[]}' + fi +} + +gh_get_run_jobs() { + local repo="$1" + local run_id="$2" + + if command -v gh &>/dev/null; then + gh api "repos/$repo/actions/runs/$run_id/jobs" 2>/dev/null + elif [[ -n "$GITHUB_TOKEN" ]]; then + curl -s -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$repo/actions/runs/$run_id/jobs" 2>/dev/null + else + echo '{"jobs":[]}' + fi +} + +gh_rerun_workflow() { + local repo="$1" + local run_id="$2" + + if command -v gh &>/dev/null; then + gh api -X POST "repos/$repo/actions/runs/$run_id/rerun" 2>/dev/null + fi +} + +gh_cancel_run() { + local repo="$1" + local run_id="$2" + + if command -v gh &>/dev/null; then + gh api -X POST "repos/$repo/actions/runs/$run_id/cancel" 2>/dev/null + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# GITLAB CI +#─────────────────────────────────────────────────────────────────────────────── + +gitlab_get_pipelines() { + local project_id="$1" + local per_page="${2:-20}" + + [[ -z "$GITLAB_TOKEN" ]] && echo '[]' && return + + curl -s -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + "https://gitlab.com/api/v4/projects/$project_id/pipelines?per_page=$per_page" 2>/dev/null +} + +gitlab_get_pipeline_jobs() { + local project_id="$1" + local pipeline_id="$2" + + [[ -z "$GITLAB_TOKEN" ]] && echo '[]' && return + + curl -s -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + "https://gitlab.com/api/v4/projects/$project_id/pipelines/$pipeline_id/jobs" 2>/dev/null +} + +gitlab_retry_pipeline() { + local project_id="$1" + local pipeline_id="$2" + + [[ -z "$GITLAB_TOKEN" ]] && return + + curl -s -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + "https://gitlab.com/api/v4/projects/$project_id/pipelines/$pipeline_id/retry" 2>/dev/null +} + +gitlab_cancel_pipeline() { + local project_id="$1" + local pipeline_id="$2" + + [[ -z "$GITLAB_TOKEN" ]] && return + + curl -s -X POST -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ + "https://gitlab.com/api/v4/projects/$project_id/pipelines/$pipeline_id/cancel" 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# JENKINS +#─────────────────────────────────────────────────────────────────────────────── + +jenkins_get_jobs() { + local jenkins_url="$1" + local user="${2:-}" + local token="${3:-$JENKINS_TOKEN}" + + local auth="" + [[ -n "$user" && -n "$token" ]] && auth="-u $user:$token" + + curl -s $auth "${jenkins_url}/api/json?tree=jobs[name,color,lastBuild[number,result,timestamp,duration]]" 2>/dev/null +} + +jenkins_get_build() { + local jenkins_url="$1" + local job_name="$2" + local build_number="$3" + + curl -s "${jenkins_url}/job/$job_name/$build_number/api/json" 2>/dev/null +} + +jenkins_trigger_build() { + local jenkins_url="$1" + local job_name="$2" + local user="${3:-}" + local token="${4:-$JENKINS_TOKEN}" + + local auth="" + [[ -n "$user" && -n "$token" ]] && auth="-u $user:$token" + + curl -s -X POST $auth "${jenkins_url}/job/$job_name/build" 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# CIRCLECI +#─────────────────────────────────────────────────────────────────────────────── + +circleci_get_pipelines() { + local project_slug="$1" # e.g., gh/owner/repo + + [[ -z "$CIRCLECI_TOKEN" ]] && echo '{"items":[]}' && return + + curl -s -H "Circle-Token: $CIRCLECI_TOKEN" \ + "https://circleci.com/api/v2/project/$project_slug/pipeline?limit=20" 2>/dev/null +} + +circleci_get_workflow() { + local pipeline_id="$1" + + [[ -z "$CIRCLECI_TOKEN" ]] && echo '{"items":[]}' && return + + curl -s -H "Circle-Token: $CIRCLECI_TOKEN" \ + "https://circleci.com/api/v2/pipeline/$pipeline_id/workflow" 2>/dev/null +} + +circleci_rerun_workflow() { + local workflow_id="$1" + + [[ -z "$CIRCLECI_TOKEN" ]] && return + + curl -s -X POST -H "Circle-Token: $CIRCLECI_TOKEN" \ + "https://circleci.com/api/v2/workflow/$workflow_id/rerun" 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# STATUS RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +render_status_icon() { + local status="$1" + + case "$status" in + success|completed|passed|SUCCESS) + printf "${BR_GREEN}✓${RST}" + ;; + failure|failed|FAILURE) + printf "${BR_RED}✗${RST}" + ;; + running|in_progress|pending) + printf "${BR_YELLOW}◐${RST}" + ;; + queued|waiting|created) + printf "${BR_CYAN}◯${RST}" + ;; + cancelled|canceled|skipped) + printf "${TEXT_MUTED}○${RST}" + ;; + *) + printf "${TEXT_MUTED}?${RST}" + ;; + esac +} + +render_status_bar() { + local jobs_json="$1" + local max_width="${2:-40}" + + if ! command -v jq &>/dev/null; then + return + fi + + local success=0 + local failure=0 + local running=0 + local other=0 + + while read -r status; do + case "$status" in + completed|success|passed) ((success++)) ;; + failure|failed) ((failure++)) ;; + in_progress|running|pending) ((running++)) ;; + *) ((other++)) ;; + esac + done < <(echo "$jobs_json" | jq -r '.[].status // .[].conclusion // "unknown"' 2>/dev/null) + + local total=$((success + failure + running + other)) + [[ $total -eq 0 ]] && return + + local success_width=$((success * max_width / total)) + local failure_width=$((failure * max_width / total)) + local running_width=$((running * max_width / total)) + local other_width=$((max_width - success_width - failure_width - running_width)) + + printf "${BR_GREEN}" + printf "%0.s█" $(seq 1 $success_width 2>/dev/null) || true + printf "${BR_RED}" + printf "%0.s█" $(seq 1 $failure_width 2>/dev/null) || true + printf "${BR_YELLOW}" + printf "%0.s█" $(seq 1 $running_width 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $other_width 2>/dev/null) || true + printf "${RST}" +} + +render_duration() { + local seconds="$1" + + if [[ $seconds -ge 3600 ]]; then + printf "%dh %dm" $((seconds / 3600)) $(((seconds % 3600) / 60)) + elif [[ $seconds -ge 60 ]]; then + printf "%dm %ds" $((seconds / 60)) $((seconds % 60)) + else + printf "%ds" "$seconds" + fi +} + +render_time_ago() { + local timestamp="$1" + + # Convert ISO timestamp to seconds + local then_epoch=$(date -d "$timestamp" +%s 2>/dev/null || echo 0) + local now_epoch=$(date +%s) + local diff=$((now_epoch - then_epoch)) + + if [[ $diff -lt 60 ]]; then + printf "just now" + elif [[ $diff -lt 3600 ]]; then + printf "%dm ago" $((diff / 60)) + elif [[ $diff -lt 86400 ]]; then + printf "%dh ago" $((diff / 3600)) + else + printf "%dd ago" $((diff / 86400)) + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# GITHUB ACTIONS DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_github_dashboard() { + local repo="$1" + + printf "${BOLD}GitHub Actions: %s${RST}\n\n" "$repo" + + if ! command -v jq &>/dev/null; then + printf "${TEXT_MUTED}jq required for parsing${RST}\n" + return + fi + + local runs=$(gh_get_runs "$repo" "" 15) + + printf "%-3s %-35s %-12s %-10s %-15s %-12s\n" "ST" "WORKFLOW" "STATUS" "DURATION" "BRANCH" "TRIGGERED" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────────────" + + echo "$runs" | jq -c '.workflow_runs[]?' 2>/dev/null | while read -r run; do + local name=$(echo "$run" | jq -r '.name') + local status=$(echo "$run" | jq -r '.status') + local conclusion=$(echo "$run" | jq -r '.conclusion // "running"') + local branch=$(echo "$run" | jq -r '.head_branch') + local created=$(echo "$run" | jq -r '.created_at') + local run_id=$(echo "$run" | jq -r '.id') + + # Calculate duration + local started=$(echo "$run" | jq -r '.run_started_at // .created_at') + local updated=$(echo "$run" | jq -r '.updated_at') + local start_epoch=$(date -d "$started" +%s 2>/dev/null || echo 0) + local end_epoch=$(date -d "$updated" +%s 2>/dev/null || echo 0) + local duration=$((end_epoch - start_epoch)) + + local display_status="$conclusion" + [[ "$status" == "in_progress" ]] && display_status="running" + + printf " " + render_status_icon "$display_status" + printf " ${BOLD}%-35s${RST} " "${name:0:35}" + + case "$display_status" in + success) printf "${BR_GREEN}%-12s${RST} " "$display_status" ;; + failure) printf "${BR_RED}%-12s${RST} " "$display_status" ;; + running) printf "${BR_YELLOW}%-12s${RST} " "$display_status" ;; + *) printf "${TEXT_MUTED}%-12s${RST} " "$display_status" ;; + esac + + printf "${TEXT_SECONDARY}%-10s${RST} " "$(render_duration $duration)" + printf "${BR_CYAN}%-15s${RST} " "${branch:0:15}" + printf "${TEXT_MUTED}%-12s${RST}\n" "$(render_time_ago "$created")" + done +} + +render_github_run_details() { + local repo="$1" + local run_id="$2" + + if ! command -v jq &>/dev/null; then + return + fi + + local jobs=$(gh_get_run_jobs "$repo" "$run_id") + + printf "\n${BOLD}Jobs for run #%s${RST}\n\n" "$run_id" + + echo "$jobs" | jq -c '.jobs[]?' 2>/dev/null | while read -r job; do + local name=$(echo "$job" | jq -r '.name') + local status=$(echo "$job" | jq -r '.status') + local conclusion=$(echo "$job" | jq -r '.conclusion // "running"') + + printf " " + render_status_icon "$conclusion" + printf " %-30s " "$name" + + # Steps + local steps=$(echo "$job" | jq -c '.steps[]?' 2>/dev/null) + echo "$steps" | while read -r step; do + local step_conclusion=$(echo "$step" | jq -r '.conclusion // "pending"') + render_status_icon "$step_conclusion" + done + printf "\n" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# PIPELINE VISUALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +render_pipeline_ascii() { + local -a stages=("$@") + + printf "\n" + + # Draw pipeline stages + for ((i=0; i<${#stages[@]}; i++)); do + IFS='|' read -r name status <<< "${stages[i]}" + + local color="$TEXT_MUTED" + case "$status" in + success|passed) color="$BR_GREEN" ;; + failure|failed) color="$BR_RED" ;; + running) color="$BR_YELLOW" ;; + pending) color="$BR_CYAN" ;; + esac + + # Stage box + printf "${color}" + printf "┌─────────────┐" + printf "${RST}" + + if [[ $i -lt $((${#stages[@]} - 1)) ]]; then + printf "──▶" + fi + done + printf "\n" + + for ((i=0; i<${#stages[@]}; i++)); do + IFS='|' read -r name status <<< "${stages[i]}" + + local color="$TEXT_MUTED" + case "$status" in + success|passed) color="$BR_GREEN" ;; + failure|failed) color="$BR_RED" ;; + running) color="$BR_YELLOW" ;; + pending) color="$BR_CYAN" ;; + esac + + printf "${color}" + printf "│ %-11s │" "${name:0:11}" + printf "${RST}" + + if [[ $i -lt $((${#stages[@]} - 1)) ]]; then + printf " " + fi + done + printf "\n" + + for ((i=0; i<${#stages[@]}; i++)); do + IFS='|' read -r name status <<< "${stages[i]}" + + local color="$TEXT_MUTED" + case "$status" in + success|passed) color="$BR_GREEN" ;; + failure|failed) color="$BR_RED" ;; + running) color="$BR_YELLOW" ;; + pending) color="$BR_CYAN" ;; + esac + + printf "${color}" + printf "│ " + render_status_icon "$status" + printf " │" + printf "${RST}" + + if [[ $i -lt $((${#stages[@]} - 1)) ]]; then + printf " " + fi + done + printf "\n" + + for ((i=0; i<${#stages[@]}; i++)); do + IFS='|' read -r name status <<< "${stages[i]}" + + local color="$TEXT_MUTED" + case "$status" in + success|passed) color="$BR_GREEN" ;; + failure|failed) color="$BR_RED" ;; + running) color="$BR_YELLOW" ;; + pending) color="$BR_CYAN" ;; + esac + + printf "${color}" + printf "└─────────────┘" + printf "${RST}" + + if [[ $i -lt $((${#stages[@]} - 1)) ]]; then + printf " " + fi + done + printf "\n\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOCAL CI SIMULATION +#─────────────────────────────────────────────────────────────────────────────── + +run_local_ci() { + local config_file="${1:-.github/workflows}" + + clear_screen + printf "${BOLD}Running Local CI Simulation${RST}\n\n" + + # Define common stages + local -a stages=("checkout" "install" "lint" "test" "build" "deploy") + local -a statuses=() + + for stage in "${stages[@]}"; do + statuses+=("$stage|pending") + done + + for ((i=0; i<${#stages[@]}; i++)); do + local stage="${stages[i]}" + statuses[i]="$stage|running" + + # Render current state + cursor_to 4 1 + render_pipeline_ascii "${statuses[@]}" + + printf " ${BR_YELLOW}Running: %s...${RST}\n" "$stage" + + # Simulate work + case "$stage" in + checkout) + sleep 1 + ;; + install) + [[ -f "package.json" ]] && npm install --silent 2>/dev/null + sleep 2 + ;; + lint) + if [[ -f "package.json" ]]; then + npm run lint --silent 2>/dev/null || { + statuses[i]="$stage|failure" + break + } + fi + sleep 1 + ;; + test) + if [[ -f "package.json" ]]; then + npm test --silent 2>/dev/null || { + statuses[i]="$stage|failure" + break + } + fi + sleep 2 + ;; + build) + [[ -f "package.json" ]] && npm run build --silent 2>/dev/null + sleep 2 + ;; + deploy) + sleep 1 + ;; + esac + + statuses[i]="$stage|success" + done + + # Final render + cursor_to 4 1 + render_pipeline_ascii "${statuses[@]}" + + # Summary + local passed=0 + local failed=0 + for status in "${statuses[@]}"; do + [[ "$status" == *"|success" ]] && ((passed++)) + [[ "$status" == *"|failure" ]] && ((failed++)) + done + + if [[ $failed -eq 0 ]]; then + printf " ${BR_GREEN}All stages passed!${RST}\n" + else + printf " ${BR_RED}Pipeline failed: %d stage(s) failed${RST}\n" "$failed" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_cicd_dashboard() { + clear_screen + cursor_hide + + printf "${BR_ORANGE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ ⚙️ CI/CD PIPELINE MONITOR ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Detect repo + local repo="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local remote=$(git remote get-url origin 2>/dev/null) + if [[ "$remote" == *github.com* ]]; then + repo=$(echo "$remote" | sed -E 's|.*github.com[:/]||' | sed 's|\.git$||') + fi + fi + + if [[ -n "$repo" ]]; then + printf " ${BOLD}Repository:${RST} ${BR_CYAN}%s${RST}\n\n" "$repo" + render_github_dashboard "$repo" + else + printf " ${TEXT_MUTED}Not in a Git repository or no remote detected${RST}\n\n" + + # Show demo pipeline + printf "${BOLD}Demo Pipeline:${RST}\n" + render_pipeline_ascii "Build|success" "Test|success" "Lint|success" "Deploy|running" + fi + + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[r]efresh [d]etails [c]ancel [t]rigger [l]ocal CI [q]uit${RST}\n" +} + +cicd_dashboard_loop() { + while true; do + render_cicd_dashboard + + if read -rsn1 -t 5 key 2>/dev/null; then + case "$key" in + r|R) continue ;; + d|D) + local repo="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local remote=$(git remote get-url origin 2>/dev/null) + repo=$(echo "$remote" | sed -E 's|.*github.com[:/]||' | sed 's|\.git$||') + fi + + if [[ -n "$repo" ]]; then + printf "\n${BR_CYAN}Run ID to view details: ${RST}" + cursor_show + read -r run_id + cursor_hide + [[ -n "$run_id" ]] && render_github_run_details "$repo" "$run_id" + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + fi + ;; + c|C) + local repo="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local remote=$(git remote get-url origin 2>/dev/null) + repo=$(echo "$remote" | sed -E 's|.*github.com[:/]||' | sed 's|\.git$||') + fi + + if [[ -n "$repo" ]]; then + printf "\n${BR_CYAN}Run ID to cancel: ${RST}" + cursor_show + read -r run_id + cursor_hide + [[ -n "$run_id" ]] && gh_cancel_run "$repo" "$run_id" + printf "${BR_YELLOW}Cancelled run #%s${RST}\n" "$run_id" + sleep 1 + fi + ;; + t|T) + local repo="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local remote=$(git remote get-url origin 2>/dev/null) + repo=$(echo "$remote" | sed -E 's|.*github.com[:/]||' | sed 's|\.git$||') + fi + + if [[ -n "$repo" ]]; then + printf "\n${BR_CYAN}Run ID to rerun: ${RST}" + cursor_show + read -r run_id + cursor_hide + [[ -n "$run_id" ]] && gh_rerun_workflow "$repo" "$run_id" + printf "${BR_GREEN}Triggered rerun for #%s${RST}\n" "$run_id" + sleep 1 + fi + ;; + l|L) + run_local_ci + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) cicd_dashboard_loop ;; + github) render_github_dashboard "$2" ;; + gitlab) gitlab_get_pipelines "$2" ;; + local) run_local_ci "$2" ;; + status) + if [[ -n "$2" ]]; then + render_github_dashboard "$2" + else + # Try to detect from git remote + local repo="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local remote=$(git remote get-url origin 2>/dev/null) + repo=$(echo "$remote" | sed -E 's|.*github.com[:/]||' | sed 's|\.git$||') + render_github_dashboard "$repo" + fi + fi + ;; + *) + printf "Usage: %s [dashboard|github|gitlab|local|status]\n" "$0" + ;; + esac +fi diff --git a/code-stats.sh b/code-stats.sh new file mode 100644 index 0000000..11d4b48 --- /dev/null +++ b/code-stats.sh @@ -0,0 +1,625 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██████╗ ██████╗ ███████╗ ███████╗████████╗ █████╗ ████████╗███████╗ +# ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝ +# ██║ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████║ ██║ ███████╗ +# ██║ ██║ ██║██║ ██║██╔══╝ ╚════██║ ██║ ██╔══██║ ██║ ╚════██║ +# ╚██████╗╚██████╔╝██████╔╝███████╗ ███████║ ██║ ██║ ██║ ██║ ███████║ +# ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD CODE STATISTICS ANALYZER v1.0 +# Project Analytics & Code Metrics Dashboard +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +declare -A LANG_EXTENSIONS=( + [bash]="sh bash zsh" + [python]="py pyw" + [javascript]="js mjs jsx" + [typescript]="ts tsx" + [go]="go" + [rust]="rs" + [c]="c h" + [cpp]="cpp cc cxx hpp hxx" + [java]="java" + [ruby]="rb" + [php]="php" + [html]="html htm" + [css]="css scss sass less" + [json]="json" + [yaml]="yml yaml" + [markdown]="md markdown" + [sql]="sql" + [swift]="swift" + [kotlin]="kt kts" +) + +declare -A LANG_COLORS=( + [bash]="208" + [python]="226" + [javascript]="226" + [typescript]="39" + [go]="51" + [rust]="208" + [c]="39" + [cpp]="39" + [java]="196" + [ruby]="196" + [php]="141" + [html]="208" + [css]="39" + [json]="226" + [yaml]="196" + [markdown]="252" + [sql]="208" + [swift]="208" + [kotlin]="141" +) + +declare -A LANG_ICONS=( + [bash]="📜" + [python]="🐍" + [javascript]="📦" + [typescript]="📘" + [go]="🐹" + [rust]="🦀" + [c]="⚙️" + [cpp]="⚙️" + [java]="☕" + [ruby]="💎" + [php]="🐘" + [html]="🌐" + [css]="🎨" + [json]="📋" + [yaml]="📄" + [markdown]="📝" + [sql]="🗃️" + [swift]="🕊️" + [kotlin]="🅺" +) + +#─────────────────────────────────────────────────────────────────────────────── +# ANALYSIS FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +detect_language() { + local file="$1" + local ext="${file##*.}" + ext="${ext,,}" + + for lang in "${!LANG_EXTENSIONS[@]}"; do + for e in ${LANG_EXTENSIONS[$lang]}; do + [[ "$ext" == "$e" ]] && echo "$lang" && return + done + done + echo "other" +} + +count_lines() { + local file="$1" + local total=0 + local code=0 + local comments=0 + local blank=0 + + while IFS= read -r line || [[ -n "$line" ]]; do + ((total++)) + + # Trim whitespace + local trimmed="${line#"${line%%[![:space:]]*}"}" + + if [[ -z "$trimmed" ]]; then + ((blank++)) + elif [[ "$trimmed" =~ ^[#//\*\;] ]] || [[ "$trimmed" =~ ^\<\!-- ]] || [[ "$trimmed" =~ ^/\* ]]; then + ((comments++)) + else + ((code++)) + fi + done < "$file" + + echo "$total $code $comments $blank" +} + +get_file_complexity() { + local file="$1" + local complexity=0 + + # Count control structures + local ifs=$(grep -cE '\b(if|else|elif|switch|case)\b' "$file" 2>/dev/null || echo 0) + local loops=$(grep -cE '\b(for|while|do|foreach|loop)\b' "$file" 2>/dev/null || echo 0) + local funcs=$(grep -cE '\b(function|def|func|fn|sub)\b' "$file" 2>/dev/null || echo 0) + local exceptions=$(grep -cE '\b(try|catch|except|finally|throw|raise)\b' "$file" 2>/dev/null || echo 0) + + complexity=$((ifs + loops + funcs * 2 + exceptions)) + echo "$complexity" +} + +analyze_directory() { + local dir="$1" + local -A lang_files=() + local -A lang_lines=() + local -A lang_code=() + local -A lang_comments=() + local -A lang_blank=() + local total_files=0 + local total_lines=0 + local total_code=0 + local total_comments=0 + local total_blank=0 + + printf "\n \033[38;5;240mScanning: %s\033[0m\n" "$dir" + + while IFS= read -r file; do + [[ ! -f "$file" ]] && continue + [[ "$file" =~ (node_modules|vendor|\.git|\.venv|__pycache__|dist|build)/ ]] && continue + + local lang=$(detect_language "$file") + [[ "$lang" == "other" ]] && continue + + local counts=$(count_lines "$file") + read -r lines code comments blank <<< "$counts" + + ((lang_files[$lang]++)) + ((lang_lines[$lang]+=lines)) + ((lang_code[$lang]+=code)) + ((lang_comments[$lang]+=comments)) + ((lang_blank[$lang]+=blank)) + ((total_files++)) + ((total_lines+=lines)) + ((total_code+=code)) + ((total_comments+=comments)) + ((total_blank+=blank)) + + done < <(find "$dir" -type f 2>/dev/null) + + # Store results in globals + declare -gA RESULT_FILES=() + declare -gA RESULT_LINES=() + declare -gA RESULT_CODE=() + declare -gA RESULT_COMMENTS=() + declare -gA RESULT_BLANK=() + + for lang in "${!lang_files[@]}"; do + RESULT_FILES[$lang]=${lang_files[$lang]} + RESULT_LINES[$lang]=${lang_lines[$lang]} + RESULT_CODE[$lang]=${lang_code[$lang]} + RESULT_COMMENTS[$lang]=${lang_comments[$lang]} + RESULT_BLANK[$lang]=${lang_blank[$lang]} + done + + TOTAL_FILES=$total_files + TOTAL_LINES=$total_lines + TOTAL_CODE=$total_code + TOTAL_COMMENTS=$total_comments + TOTAL_BLANK=$total_blank +} + +#─────────────────────────────────────────────────────────────────────────────── +# VISUALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +format_number() { + local num="$1" + if [[ $num -ge 1000000 ]]; then + printf "%.1fM" "$(echo "$num / 1000000" | bc -l)" + elif [[ $num -ge 1000 ]]; then + printf "%.1fK" "$(echo "$num / 1000" | bc -l)" + else + echo "$num" + fi +} + +draw_bar() { + local value="$1" + local max="$2" + local width="$3" + local color="$4" + + local filled=$((value * width / max)) + [[ $filled -eq 0 && $value -gt 0 ]] && filled=1 + + printf "\033[38;5;${color}m" + for ((i=0; i<filled; i++)); do printf "█"; done + printf "\033[38;5;240m" + for ((i=filled; i<width; i++)); do printf "░"; done + printf "\033[0m" +} + +render_header() { + printf "\033[1;38;5;214m" + cat << 'EOF' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 📊 CODE STATISTICS ANALYZER ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +EOF + printf "\033[0m" +} + +render_summary() { + local dir="$1" + + printf "\n \033[1;38;5;39m📁 Project: \033[0m%s\n" "$dir" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + # Summary boxes + printf " ┌────────────┬────────────┬────────────┬────────────┐\n" + printf " │ \033[1;38;5;39m📄 Files \033[0m │ \033[1;38;5;46m📝 Lines \033[0m │ \033[1;38;5;226m💻 Code \033[0m │ \033[1;38;5;208m💬 Comment\033[0m │\n" + printf " ├────────────┼────────────┼────────────┼────────────┤\n" + printf " │ %6s │ %6s │ %6s │ %6s │\n" \ + "$(format_number $TOTAL_FILES)" \ + "$(format_number $TOTAL_LINES)" \ + "$(format_number $TOTAL_CODE)" \ + "$(format_number $TOTAL_COMMENTS)" + printf " └────────────┴────────────┴────────────┴────────────┘\n" +} + +render_language_breakdown() { + printf "\n \033[1;38;5;201m📈 Language Breakdown\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + # Sort languages by lines + local sorted_langs=() + for lang in "${!RESULT_LINES[@]}"; do + sorted_langs+=("${RESULT_LINES[$lang]}:$lang") + done + IFS=$'\n' sorted_langs=($(sort -t: -k1 -rn <<< "${sorted_langs[*]}")); unset IFS + + # Find max for bar scaling + local max_lines=1 + for entry in "${sorted_langs[@]}"; do + local lines="${entry%%:*}" + [[ $lines -gt $max_lines ]] && max_lines=$lines + done + + for entry in "${sorted_langs[@]}"; do + local lines="${entry%%:*}" + local lang="${entry#*:}" + local files=${RESULT_FILES[$lang]:-0} + local code=${RESULT_CODE[$lang]:-0} + local color=${LANG_COLORS[$lang]:-252} + local icon=${LANG_ICONS[$lang]:-"📄"} + local pct=$((lines * 100 / TOTAL_LINES)) + + printf " %s %-12s " "$icon" "$lang" + draw_bar "$lines" "$max_lines" 30 "$color" + printf " \033[38;5;252m%6s lines\033[0m \033[38;5;240m(%2d%%) %d files\033[0m\n" \ + "$(format_number $lines)" "$pct" "$files" + done +} + +render_code_composition() { + printf "\n \033[1;38;5;46m📊 Code Composition\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + local code_pct=$((TOTAL_CODE * 100 / TOTAL_LINES)) + local comment_pct=$((TOTAL_COMMENTS * 100 / TOTAL_LINES)) + local blank_pct=$((TOTAL_BLANK * 100 / TOTAL_LINES)) + + # Visual pie representation + local bar_width=50 + + printf " \033[38;5;46mCode \033[0m " + draw_bar "$TOTAL_CODE" "$TOTAL_LINES" "$bar_width" "46" + printf " %3d%%\n" "$code_pct" + + printf " \033[38;5;208mComments \033[0m " + draw_bar "$TOTAL_COMMENTS" "$TOTAL_LINES" "$bar_width" "208" + printf " %3d%%\n" "$comment_pct" + + printf " \033[38;5;240mBlank \033[0m " + draw_bar "$TOTAL_BLANK" "$TOTAL_LINES" "$bar_width" "240" + printf " %3d%%\n" "$blank_pct" +} + +render_file_types() { + printf "\n \033[1;38;5;226m📁 File Type Distribution\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + # Grid display of file counts + local col=0 + for lang in "${!RESULT_FILES[@]}"; do + local files=${RESULT_FILES[$lang]} + local icon=${LANG_ICONS[$lang]:-"📄"} + local color=${LANG_COLORS[$lang]:-252} + + printf " %s \033[38;5;${color}m%-10s\033[0m \033[38;5;240m%4d\033[0m" "$icon" "$lang" "$files" + + ((col++)) + if [[ $col -ge 3 ]]; then + printf "\n" + col=0 + else + printf " " + fi + done + [[ $col -ne 0 ]] && printf "\n" +} + +render_largest_files() { + local dir="$1" + printf "\n \033[1;38;5;196m📏 Largest Files\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + local count=0 + while IFS= read -r line; do + local lines="${line%% *}" + local file="${line#* }" + file="${file#$dir/}" + + local lang=$(detect_language "$file") + local color=${LANG_COLORS[$lang]:-252} + local icon=${LANG_ICONS[$lang]:-"📄"} + + printf " %s \033[38;5;${color}m%6s lines\033[0m %-50s\n" "$icon" "$(format_number $lines)" "${file:0:50}" + + ((count++)) + [[ $count -ge 10 ]] && break + done < <(find "$dir" -type f \( -name "*.sh" -o -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" -o -name "*.rs" -o -name "*.java" \) \ + ! -path "*node_modules*" ! -path "*.git*" ! -path "*vendor*" \ + -exec wc -l {} \; 2>/dev/null | sort -rn) +} + +render_complexity_analysis() { + local dir="$1" + printf "\n \033[1;38;5;141m🧩 Complexity Analysis\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + local high_complexity=() + local medium_complexity=() + local low_complexity=() + + while IFS= read -r file; do + [[ ! -f "$file" ]] && continue + [[ "$file" =~ (node_modules|vendor|\.git)/ ]] && continue + + local complexity=$(get_file_complexity "$file") + local fname="${file#$dir/}" + + if [[ $complexity -ge 50 ]]; then + high_complexity+=("$complexity:$fname") + elif [[ $complexity -ge 20 ]]; then + medium_complexity+=("$complexity:$fname") + else + low_complexity+=("$complexity:$fname") + fi + done < <(find "$dir" -type f \( -name "*.sh" -o -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.go" \) 2>/dev/null | head -100) + + printf " \033[38;5;196m● High Complexity:\033[0m %d files\n" "${#high_complexity[@]}" + printf " \033[38;5;226m● Medium Complexity:\033[0m %d files\n" "${#medium_complexity[@]}" + printf " \033[38;5;46m● Low Complexity:\033[0m %d files\n" "${#low_complexity[@]}" + + if [[ ${#high_complexity[@]} -gt 0 ]]; then + printf "\n \033[38;5;240mHighest complexity files:\033[0m\n" + IFS=$'\n' sorted=($(sort -t: -k1 -rn <<< "${high_complexity[*]}")); unset IFS + for ((i=0; i<3 && i<${#sorted[@]}; i++)); do + local c="${sorted[$i]%%:*}" + local f="${sorted[$i]#*:}" + printf " \033[38;5;196m%3d\033[0m %s\n" "$c" "${f:0:50}" + done + fi +} + +render_git_stats() { + local dir="$1" + + if [[ ! -d "$dir/.git" ]]; then + return + fi + + printf "\n \033[1;38;5;208m📜 Git Statistics\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + cd "$dir" || return + + local total_commits=$(git rev-list --count HEAD 2>/dev/null || echo "0") + local contributors=$(git shortlog -sn 2>/dev/null | wc -l) + local first_commit=$(git log --reverse --format="%as" 2>/dev/null | head -1) + local last_commit=$(git log -1 --format="%as" 2>/dev/null) + local branches=$(git branch -a 2>/dev/null | wc -l) + + printf " 📊 Total Commits: %s\n" "$total_commits" + printf " 👥 Contributors: %s\n" "$contributors" + printf " 🌿 Branches: %s\n" "$branches" + printf " 📅 First Commit: %s\n" "$first_commit" + printf " 📅 Last Commit: %s\n" "$last_commit" + + printf "\n \033[38;5;240mTop Contributors:\033[0m\n" + git shortlog -sn 2>/dev/null | head -5 | while read -r count name; do + printf " \033[38;5;39m%4d\033[0m %s\n" "$count" "$name" + done + + cd - > /dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# EXPORT FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +export_json() { + local dir="$1" + local output="$2" + + { + printf '{\n' + printf ' "project": "%s",\n' "$dir" + printf ' "analyzed_at": "%s",\n' "$(date -Iseconds)" + printf ' "summary": {\n' + printf ' "total_files": %d,\n' "$TOTAL_FILES" + printf ' "total_lines": %d,\n' "$TOTAL_LINES" + printf ' "code_lines": %d,\n' "$TOTAL_CODE" + printf ' "comment_lines": %d,\n' "$TOTAL_COMMENTS" + printf ' "blank_lines": %d\n' "$TOTAL_BLANK" + printf ' },\n' + printf ' "languages": {\n' + + local first=true + for lang in "${!RESULT_FILES[@]}"; do + $first || printf ',\n' + printf ' "%s": {\n' "$lang" + printf ' "files": %d,\n' "${RESULT_FILES[$lang]}" + printf ' "lines": %d,\n' "${RESULT_LINES[$lang]}" + printf ' "code": %d,\n' "${RESULT_CODE[$lang]}" + printf ' "comments": %d\n' "${RESULT_COMMENTS[$lang]}" + printf ' }' + first=false + done + + printf '\n }\n' + printf '}\n' + } > "$output" + + printf "\n \033[38;5;46m✓ Exported to: %s\033[0m\n" "$output" +} + +export_markdown() { + local dir="$1" + local output="$2" + + { + printf "# Code Statistics Report\n\n" + printf "**Project:** %s\n" "$dir" + printf "**Analyzed:** %s\n\n" "$(date)" + + printf "## Summary\n\n" + printf "| Metric | Value |\n" + printf "|--------|-------|\n" + printf "| Files | %d |\n" "$TOTAL_FILES" + printf "| Lines | %d |\n" "$TOTAL_LINES" + printf "| Code | %d |\n" "$TOTAL_CODE" + printf "| Comments | %d |\n" "$TOTAL_COMMENTS" + printf "| Blank | %d |\n\n" "$TOTAL_BLANK" + + printf "## Languages\n\n" + printf "| Language | Files | Lines | Code | Comments |\n" + printf "|----------|-------|-------|------|----------|\n" + + for lang in "${!RESULT_FILES[@]}"; do + printf "| %s | %d | %d | %d | %d |\n" \ + "$lang" "${RESULT_FILES[$lang]}" "${RESULT_LINES[$lang]}" \ + "${RESULT_CODE[$lang]}" "${RESULT_COMMENTS[$lang]}" + done + + } > "$output" + + printf "\n \033[38;5;46m✓ Exported to: %s\033[0m\n" "$output" +} + +#─────────────────────────────────────────────────────────────────────────────── +# INTERACTIVE MODE +#─────────────────────────────────────────────────────────────────────────────── + +interactive_mode() { + local dir="$1" + + while true; do + clear + render_header + analyze_directory "$dir" + render_summary "$dir" + render_language_breakdown + render_code_composition + render_file_types + render_largest_files "$dir" + render_complexity_analysis "$dir" + render_git_stats "$dir" + + printf "\n \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + printf " \033[38;5;39mr\033[0m Refresh \033[38;5;46mj\033[0m Export JSON \033[38;5;226mm\033[0m Export Markdown \033[38;5;240mq\033[0m Quit\n" + + read -rsn1 key + case "$key" in + r|R) continue ;; + j|J) export_json "$dir" "code-stats.json" ;; + m|M) export_markdown "$dir" "code-stats.md" ;; + q|Q) clear; exit 0 ;; + esac + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'EOF' + + BLACKROAD CODE STATISTICS ANALYZER + ═══════════════════════════════════ + + Usage: code-stats.sh [options] [directory] + + Options: + -i, --interactive Interactive mode with refresh + -j, --json <file> Export results to JSON + -m, --markdown <file> Export results to Markdown + -h, --help Show this help + + Features: + • Line count analysis (code, comments, blank) + • Language detection and breakdown + • Complexity analysis + • Largest files identification + • Git statistics integration + • Export to JSON/Markdown + + Examples: + code-stats.sh # Analyze current directory + code-stats.sh ./my-project # Analyze specific directory + code-stats.sh -i # Interactive mode + code-stats.sh -j stats.json # Export to JSON + +EOF +} + +main() { + local dir="$(pwd)" + local interactive=false + local json_out="" + local md_out="" + + while [[ $# -gt 0 ]]; do + case "$1" in + -i|--interactive) interactive=true; shift ;; + -j|--json) json_out="$2"; shift 2 ;; + -m|--markdown) md_out="$2"; shift 2 ;; + -h|--help) show_help; exit 0 ;; + *) dir="$1"; shift ;; + esac + done + + if [[ ! -d "$dir" ]]; then + printf "\033[31mError: Directory not found: %s\033[0m\n" "$dir" + exit 1 + fi + + dir="$(cd "$dir" && pwd)" + + if $interactive; then + interactive_mode "$dir" + else + render_header + analyze_directory "$dir" + render_summary "$dir" + render_language_breakdown + render_code_composition + render_file_types + render_largest_files "$dir" + render_complexity_analysis "$dir" + render_git_stats "$dir" + + [[ -n "$json_out" ]] && export_json "$dir" "$json_out" + [[ -n "$md_out" ]] && export_markdown "$dir" "$md_out" + fi + + printf "\n" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/command-center.sh b/command-center.sh new file mode 100644 index 0000000..8f5a8dc --- /dev/null +++ b/command-center.sh @@ -0,0 +1,476 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ +# ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗ +# ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║ +# ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║ +# ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝ +# ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD COMMAND CENTER v3.0 ULTIMATE +# Unified Control Interface - All Systems Under One Roof +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source all libraries +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" +[[ -f "$SCRIPT_DIR/lib-cache.sh" ]] && source "$SCRIPT_DIR/lib-cache.sh" +[[ -f "$SCRIPT_DIR/lib-parallel.sh" ]] && source "$SCRIPT_DIR/lib-parallel.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +COMMAND_CENTER_VERSION="3.0.0" +STARTUP_TIME=$(date +%s) + +# Module availability +declare -A MODULES=( + [analytics]="analytics-dashboard.sh|📊 Analytics" + [ai]="ai-insights-engine.sh|🧠 AI Insights" + [security]="security-ops.sh|🔒 Security" + [workflows]="workflow-engine.sh|⚡ Workflows" + [3d]="terminal-3d.sh|🎮 3D Viz" + [plugins]="plugin-system.sh|🔌 Plugins" + [health]="health-check.sh|🏥 Health" + [export]="data-export.sh|📤 Export" + [notify]="notification-system.sh|🔔 Notify" + [themes]="themes-premium.sh|🎨 Themes" + [logs]="log-streaming.sh|📜 Logs" +) + +# Current state +CURRENT_MODULE="home" +CURRENT_TAB=1 +COMMAND_MODE=false +COMMAND_BUFFER="" + +#─────────────────────────────────────────────────────────────────────────────── +# ANIMATED INTRO +#─────────────────────────────────────────────────────────────────────────────── + +play_intro() { + clear_screen + cursor_hide + + local width=80 + local height=24 + + # Matrix-style initialization effect + for ((frame=0; frame<30; frame++)); do + cursor_to 1 1 + + for ((y=0; y<height; y++)); do + for ((x=0; x<width; x++)); do + if [[ $((RANDOM % 100)) -lt $((frame * 3)) ]]; then + printf " " + else + local chars="01アイウエオカキクケコ" + local char="${chars:$((RANDOM % ${#chars})):1}" + printf "\033[38;2;0;$((50 + RANDOM % 200));$((RANDOM % 100))m%s" "$char" + fi + done + printf "\n" + done + + sleep 0.03 + done + + # Fade in logo + for brightness in 30 60 90 120 150 180 210 240 255; do + clear_screen + printf "\033[38;2;%d;%d;%dm" "$brightness" "$((brightness/2))" "$((brightness/3))" + + cat << 'LOGO' + + + ██████╗ ██╗ █████╗ ██████╗██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ + ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔══██╗██╔═══██╗██╔══██╗██╔══██╗ + ██████╔╝██║ ███████║██║ █████╔╝ ██████╔╝██║ ██║███████║██║ ██║ + ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ██╔══██╗██║ ██║██╔══██║██║ ██║ + ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗██║ ██║╚██████╔╝██║ ██║██████╔╝ + ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ + + ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ + ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗ + ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║ + ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ + + ██████╗███████╗███╗ ██╗████████╗███████╗██████╗ + ██╔════╝██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔══██╗ + ██║ █████╗ ██╔██╗ ██║ ██║ █████╗ ██████╔╝ + ██║ ██╔══╝ ██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗ + ╚██████╗███████╗██║ ╚████║ ██║ ███████╗██║ ██║ + ╚═════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + +LOGO + printf "${RST}" + sleep 0.05 + done + + # Show version + printf "\n\n" + printf " ${TEXT_MUTED}Version %s${RST}\n" "$COMMAND_CENTER_VERSION" + printf " ${TEXT_SECONDARY}Initializing all systems...${RST}\n\n" + + # Loading bar + local modules_count=${#MODULES[@]} + local loaded=0 + + for module in "${!MODULES[@]}"; do + ((loaded++)) + local pct=$((loaded * 100 / modules_count)) + local filled=$((pct * 40 / 100)) + + printf "\r ${BR_CYAN}" + printf "%0.s█" $(seq 1 "$filled" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $((40 - filled)) 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%% Loading %s...${RST}" "$pct" "$module" + + sleep 0.1 + done + + printf "\n\n ${BR_GREEN}✓ All systems online${RST}\n" + sleep 0.5 +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD PANELS +#─────────────────────────────────────────────────────────────────────────────── + +render_header() { + local uptime_secs=$(($(date +%s) - STARTUP_TIME)) + local uptime_str=$(time_ago "$uptime_secs" 2>/dev/null || echo "${uptime_secs}s") + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ║\n" + printf "║ ██╔════╝ ██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗ ║\n" + printf "║ ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║ ║\n" + printf "║ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║ ║\n" + printf "║ ╚██████╗ ╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝ ║\n" + printf "║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ║\n" + printf "║ ${BR_ORANGE}C E N T E R v%s${BR_CYAN} ║\n" "$COMMAND_CENTER_VERSION" + printf "╚══════════════════════════════════════════════════════════════════════════════╝${RST}\n" + printf " ${TEXT_MUTED}Session: %s │ Uptime: %s │ Modules: %d${RST}\n\n" "$(date '+%H:%M:%S')" "$uptime_str" "${#MODULES[@]}" +} + +render_system_status() { + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + + printf "${BR_PURPLE}┌─ SYSTEM STATUS ──────────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_PURPLE}│${RST} ${BR_PURPLE}│${RST}\n" + + # CPU Bar + local cpu_color="$BR_GREEN" + [[ $cpu -gt 70 ]] && cpu_color="$BR_YELLOW" + [[ $cpu -gt 85 ]] && cpu_color="$BR_RED" + + printf "${BR_PURPLE}│${RST} ${BOLD}CPU${RST} " + local cpu_filled=$((cpu * 30 / 100)) + printf "${cpu_color}" + printf "%0.s█" $(seq 1 "$cpu_filled" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $((30 - cpu_filled)) 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%%${RST} ${BR_PURPLE}│${RST}\n" "$cpu" + + # Memory Bar + local mem_color="$BR_GREEN" + [[ $mem -gt 70 ]] && mem_color="$BR_YELLOW" + [[ $mem -gt 85 ]] && mem_color="$BR_RED" + + printf "${BR_PURPLE}│${RST} ${BOLD}MEM${RST} " + local mem_filled=$((mem * 30 / 100)) + printf "${mem_color}" + printf "%0.s█" $(seq 1 "$mem_filled" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $((30 - mem_filled)) 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%%${RST} ${BR_PURPLE}│${RST}\n" "$mem" + + # Disk Bar + local disk_color="$BR_GREEN" + [[ $disk -gt 80 ]] && disk_color="$BR_YELLOW" + [[ $disk -gt 90 ]] && disk_color="$BR_RED" + + printf "${BR_PURPLE}│${RST} ${BOLD}DISK${RST} " + local disk_filled=$((disk * 30 / 100)) + printf "${disk_color}" + printf "%0.s█" $(seq 1 "$disk_filled" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $((30 - disk_filled)) 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%%${RST} ${BR_PURPLE}│${RST}\n" "$disk" + + printf "${BR_PURPLE}│${RST} ${BR_PURPLE}│${RST}\n" + printf "${BR_PURPLE}└──────────────────────────────────────────────────────────────────────────┘${RST}\n\n" +} + +render_module_grid() { + printf "${BR_ORANGE}┌─ MODULES ─────────────────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_ORANGE}│${RST} ${BR_ORANGE}│${RST}\n" + + local col=0 + local row_content="" + + for module in "${!MODULES[@]}"; do + local info="${MODULES[$module]}" + local file=$(echo "$info" | cut -d'|' -f1) + local label=$(echo "$info" | cut -d'|' -f2) + + local available="${BR_GREEN}●${RST}" + [[ ! -f "$SCRIPT_DIR/$file" ]] && available="${BR_RED}○${RST}" + + row_content+=$(printf " ${available} ${BR_CYAN}%-2s${RST} ${TEXT_SECONDARY}%-12s${RST}" "${module:0:2}" "$label") + + ((col++)) + if [[ $col -ge 4 ]]; then + printf "${BR_ORANGE}│${RST}%s${BR_ORANGE}│${RST}\n" "$row_content" + row_content="" + col=0 + fi + done + + [[ -n "$row_content" ]] && printf "${BR_ORANGE}│${RST}%-74s${BR_ORANGE}│${RST}\n" "$row_content" + + printf "${BR_ORANGE}│${RST} ${BR_ORANGE}│${RST}\n" + printf "${BR_ORANGE}└──────────────────────────────────────────────────────────────────────────┘${RST}\n\n" +} + +render_quick_actions() { + printf "${BR_GREEN}┌─ QUICK ACTIONS ───────────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_YELLOW}[1]${RST} Analytics ${BR_YELLOW}[2]${RST} AI Insights ${BR_YELLOW}[3]${RST} Security ${BR_YELLOW}[4]${RST} Workflows ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_YELLOW}[5]${RST} 3D Viz ${BR_YELLOW}[6]${RST} Plugins ${BR_YELLOW}[7]${RST} Health ${BR_YELLOW}[8]${RST} Logs ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_PURPLE}[t]${RST} Themes ${BR_PURPLE}[e]${RST} Export ${BR_PURPLE}[n]${RST} Notify ${BR_PURPLE}[:]${RST} Command ${BR_PURPLE}[q]${RST} Quit ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}│${RST} ${BR_GREEN}│${RST}\n" + printf "${BR_GREEN}└──────────────────────────────────────────────────────────────────────────┘${RST}\n" +} + +render_command_line() { + if [[ "$COMMAND_MODE" == "true" ]]; then + printf "\n${BR_YELLOW}:${RST}${COMMAND_BUFFER}█" + else + printf "\n${TEXT_MUTED}Press ':' for command mode${RST}" + fi +} + +render_home() { + clear_screen + render_header + render_system_status + render_module_grid + render_quick_actions + render_command_line +} + +#─────────────────────────────────────────────────────────────────────────────── +# MODULE LAUNCHER +#─────────────────────────────────────────────────────────────────────────────── + +launch_module() { + local module_key="$1" + + local info="${MODULES[$module_key]:-}" + [[ -z "$info" ]] && return 1 + + local file=$(echo "$info" | cut -d'|' -f1) + local label=$(echo "$info" | cut -d'|' -f2) + + local script="$SCRIPT_DIR/$file" + [[ ! -f "$script" ]] && { + printf "${BR_RED}Module not found: %s${RST}\n" "$file" + sleep 1 + return 1 + } + + CURRENT_MODULE="$module_key" + + # Launch the module + bash "$script" + + CURRENT_MODULE="home" +} + +#─────────────────────────────────────────────────────────────────────────────── +# COMMAND PROCESSOR +#─────────────────────────────────────────────────────────────────────────────── + +process_command() { + local cmd="$1" + + case "$cmd" in + q|quit|exit) + return 1 + ;; + help|h|\?) + show_help + ;; + modules|mods) + for m in "${!MODULES[@]}"; do + echo "$m: ${MODULES[$m]}" + done + read -rsn1 + ;; + open\ *) + local module="${cmd#open }" + launch_module "$module" + ;; + refresh|r) + # Refresh current view + ;; + clear|cls) + clear_screen + ;; + status) + printf "CPU: %s%% | MEM: %s%% | DISK: %s%%\n" \ + "$(get_cpu_usage)" "$(get_memory_usage)" "$(get_disk_usage)" + read -rsn1 + ;; + version|ver) + printf "BlackRoad Command Center v%s\n" "$COMMAND_CENTER_VERSION" + read -rsn1 + ;; + *) + printf "${BR_RED}Unknown command: %s${RST}\n" "$cmd" + sleep 1 + ;; + esac + + return 0 +} + +show_help() { + clear_screen + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════╗\n" + printf "║ COMMAND CENTER HELP ║\n" + printf "╚══════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + printf "${BOLD}Quick Keys:${RST}\n" + printf " ${BR_YELLOW}1-8${RST} Launch modules (Analytics, AI, Security, etc.)\n" + printf " ${BR_YELLOW}t${RST} Theme selector\n" + printf " ${BR_YELLOW}e${RST} Export data\n" + printf " ${BR_YELLOW}n${RST} Notifications\n" + printf " ${BR_YELLOW}:${RST} Enter command mode\n" + printf " ${BR_YELLOW}q${RST} Quit\n" + + printf "\n${BOLD}Commands (in : mode):${RST}\n" + printf " ${BR_CYAN}open <module>${RST} Launch a module\n" + printf " ${BR_CYAN}modules${RST} List all modules\n" + printf " ${BR_CYAN}status${RST} Show system status\n" + printf " ${BR_CYAN}refresh${RST} Refresh display\n" + printf " ${BR_CYAN}clear${RST} Clear screen\n" + printf " ${BR_CYAN}help${RST} Show this help\n" + printf " ${BR_CYAN}quit${RST} Exit command center\n" + + printf "\n${TEXT_MUTED}Press any key to return...${RST}" + read -rsn1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN LOOP +#─────────────────────────────────────────────────────────────────────────────── + +main_loop() { + while true; do + render_home + + if [[ "$COMMAND_MODE" == "true" ]]; then + # Command mode input + read -rsn1 key + + case "$key" in + $'\e') # Escape - exit command mode + COMMAND_MODE=false + COMMAND_BUFFER="" + ;; + $'\177'|$'\b') # Backspace + COMMAND_BUFFER="${COMMAND_BUFFER%?}" + ;; + '') # Enter + COMMAND_MODE=false + if ! process_command "$COMMAND_BUFFER"; then + cursor_show + return + fi + COMMAND_BUFFER="" + ;; + *) + COMMAND_BUFFER+="$key" + ;; + esac + else + # Normal mode input + if read -rsn1 -t 2 key 2>/dev/null; then + case "$key" in + 1) launch_module "analytics" ;; + 2) launch_module "ai" ;; + 3) launch_module "security" ;; + 4) launch_module "workflows" ;; + 5) launch_module "3d" ;; + 6) launch_module "plugins" ;; + 7) launch_module "health" ;; + 8) launch_module "logs" ;; + t|T) launch_module "themes" ;; + e|E) launch_module "export" ;; + n|N) launch_module "notify" ;; + :) + COMMAND_MODE=true + COMMAND_BUFFER="" + ;; + q|Q) + cursor_show + clear_screen + printf "${BR_GREEN}Thanks for using BlackRoad Command Center!${RST}\n" + return + ;; + \?|h|H) + show_help + ;; + esac + fi + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-run}" in + run) + cursor_hide + stty -echo 2>/dev/null + + # Play intro animation + [[ "$2" != "--skip-intro" ]] && play_intro + + # Run main loop + main_loop + + stty echo 2>/dev/null + cursor_show + ;; + quick) + # Quick start without intro + cursor_hide + stty -echo 2>/dev/null + main_loop + stty echo 2>/dev/null + cursor_show + ;; + *) + printf "Usage: %s [run|quick]\n" "$0" + printf " %s run # Full experience with intro\n" "$0" + printf " %s quick # Skip intro animation\n" "$0" + ;; + esac +fi diff --git a/crypto-trading.sh b/crypto-trading.sh new file mode 100644 index 0000000..f54c056 --- /dev/null +++ b/crypto-trading.sh @@ -0,0 +1,775 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗██████╗ ██╗ ██╗██████╗ ████████╗ ██████╗ ████████╗██████╗ █████╗ ██████╗ ███████╗ +# ██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗╚══██╔══╝██╔═══██╗ ╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██╔════╝ +# ██║ ██████╔╝ ╚████╔╝ ██████╔╝ ██║ ██║ ██║ ██║ ██████╔╝███████║██║ ██║█████╗ +# ██║ ██╔══██╗ ╚██╔╝ ██╔═══╝ ██║ ██║ ██║ ██║ ██╔══██╗██╔══██║██║ ██║██╔══╝ +# ╚██████╗██║ ██║ ██║ ██║ ██║ ╚██████╔╝ ██║ ██║ ██║██║ ██║██████╔╝███████╗ +# ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD CRYPTO TRADING DASHBOARD v3.0 +# Real-time Cryptocurrency Prices, Charts & Portfolio Tracking +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +CRYPTO_CACHE_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/cache/crypto" +PORTFOLIO_FILE="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/portfolio.json" +ALERTS_FILE="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/crypto_alerts.json" + +mkdir -p "$CRYPTO_CACHE_DIR" 2>/dev/null + +# CoinGecko API (free, no key required) +COINGECKO_API="https://api.coingecko.com/api/v3" + +# Default watchlist +DEFAULT_COINS=("bitcoin" "ethereum" "solana" "cardano" "polkadot" "avalanche-2" "chainlink" "polygon") + +# Currency +CURRENCY="${CRYPTO_CURRENCY:-usd}" + +#─────────────────────────────────────────────────────────────────────────────── +# API FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +fetch_price() { + local coin="$1" + local cache_file="$CRYPTO_CACHE_DIR/${coin}_price.json" + local cache_age=60 # 1 minute cache + + # Check cache + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) + if [[ $file_age -lt $cache_age ]]; then + cat "$cache_file" + return + fi + fi + + # Fetch from API + local result=$(curl -s --connect-timeout 5 "$COINGECKO_API/simple/price?ids=$coin&vs_currencies=$CURRENCY&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true" 2>/dev/null) + + [[ -n "$result" ]] && echo "$result" > "$cache_file" + echo "$result" +} + +fetch_coin_details() { + local coin="$1" + local cache_file="$CRYPTO_CACHE_DIR/${coin}_details.json" + local cache_age=300 # 5 minute cache + + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) + if [[ $file_age -lt $cache_age ]]; then + cat "$cache_file" + return + fi + fi + + local result=$(curl -s --connect-timeout 5 "$COINGECKO_API/coins/$coin?localization=false&tickers=false&community_data=false&developer_data=false" 2>/dev/null) + + [[ -n "$result" ]] && echo "$result" > "$cache_file" + echo "$result" +} + +fetch_market_chart() { + local coin="$1" + local days="${2:-1}" + local cache_file="$CRYPTO_CACHE_DIR/${coin}_chart_${days}d.json" + local cache_age=300 + + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) + if [[ $file_age -lt $cache_age ]]; then + cat "$cache_file" + return + fi + fi + + local result=$(curl -s --connect-timeout 5 "$COINGECKO_API/coins/$coin/market_chart?vs_currency=$CURRENCY&days=$days" 2>/dev/null) + + [[ -n "$result" ]] && echo "$result" > "$cache_file" + echo "$result" +} + +fetch_top_coins() { + local count="${1:-20}" + local cache_file="$CRYPTO_CACHE_DIR/top_coins.json" + local cache_age=120 + + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) + if [[ $file_age -lt $cache_age ]]; then + cat "$cache_file" + return + fi + fi + + local result=$(curl -s --connect-timeout 5 "$COINGECKO_API/coins/markets?vs_currency=$CURRENCY&order=market_cap_desc&per_page=$count&page=1&sparkline=true" 2>/dev/null) + + [[ -n "$result" ]] && echo "$result" > "$cache_file" + echo "$result" +} + +fetch_global_data() { + local cache_file="$CRYPTO_CACHE_DIR/global.json" + local cache_age=120 + + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0))) + if [[ $file_age -lt $cache_age ]]; then + cat "$cache_file" + return + fi + fi + + local result=$(curl -s --connect-timeout 5 "$COINGECKO_API/global" 2>/dev/null) + + [[ -n "$result" ]] && echo "$result" > "$cache_file" + echo "$result" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DATA PARSING +#─────────────────────────────────────────────────────────────────────────────── + +parse_price() { + local json="$1" + local coin="$2" + + if command -v jq &>/dev/null; then + local price=$(echo "$json" | jq -r ".$coin.$CURRENCY // 0" 2>/dev/null) + local change=$(echo "$json" | jq -r ".$coin.${CURRENCY}_24h_change // 0" 2>/dev/null) + local volume=$(echo "$json" | jq -r ".$coin.${CURRENCY}_24h_vol // 0" 2>/dev/null) + local mcap=$(echo "$json" | jq -r ".$coin.${CURRENCY}_market_cap // 0" 2>/dev/null) + echo "$price|$change|$volume|$mcap" + else + echo "0|0|0|0" + fi +} + +format_price() { + local price="$1" + + if command -v bc &>/dev/null; then + if (( $(echo "$price >= 1" | bc -l) )); then + printf "%.2f" "$price" + elif (( $(echo "$price >= 0.01" | bc -l) )); then + printf "%.4f" "$price" + else + printf "%.8f" "$price" + fi + else + printf "%.2f" "$price" + fi +} + +format_large_number() { + local num="$1" + + if command -v bc &>/dev/null && [[ -n "$num" ]] && [[ "$num" != "null" ]]; then + if (( $(echo "$num >= 1000000000000" | bc -l 2>/dev/null || echo 0) )); then + printf "%.2fT" "$(echo "$num / 1000000000000" | bc -l)" + elif (( $(echo "$num >= 1000000000" | bc -l 2>/dev/null || echo 0) )); then + printf "%.2fB" "$(echo "$num / 1000000000" | bc -l)" + elif (( $(echo "$num >= 1000000" | bc -l 2>/dev/null || echo 0) )); then + printf "%.2fM" "$(echo "$num / 1000000" | bc -l)" + elif (( $(echo "$num >= 1000" | bc -l 2>/dev/null || echo 0) )); then + printf "%.2fK" "$(echo "$num / 1000" | bc -l)" + else + printf "%.2f" "$num" + fi + else + echo "N/A" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# SPARKLINE CHART +#─────────────────────────────────────────────────────────────────────────────── + +render_sparkline() { + local -a data=("$@") + local width="${SPARKLINE_WIDTH:-40}" + local height="${SPARKLINE_HEIGHT:-8}" + + [[ ${#data[@]} -eq 0 ]] && return + + # Find min/max + local min="${data[0]}" + local max="${data[0]}" + for val in "${data[@]}"; do + (( $(echo "$val < $min" | bc -l 2>/dev/null || echo 0) )) && min="$val" + (( $(echo "$val > $max" | bc -l 2>/dev/null || echo 0) )) && max="$val" + done + + local range=$(echo "$max - $min" | bc -l 2>/dev/null || echo 1) + [[ "$range" == "0" ]] && range=1 + + # Sample data points + local step=$((${#data[@]} / width)) + [[ $step -lt 1 ]] && step=1 + + # Build chart + local -a chart=() + for ((row=height-1; row>=0; row--)); do + local line="" + for ((col=0; col<width; col++)); do + local idx=$((col * step)) + [[ $idx -ge ${#data[@]} ]] && idx=$((${#data[@]} - 1)) + + local val="${data[$idx]}" + local normalized=$(echo "($val - $min) * $height / $range" | bc -l 2>/dev/null || echo 0) + normalized=${normalized%.*} + [[ -z "$normalized" ]] && normalized=0 + + if [[ $normalized -ge $row ]]; then + line+="█" + else + line+=" " + fi + done + chart+=("$line") + done + + # Determine color based on trend + local first="${data[0]}" + local last="${data[-1]}" + local color="$BR_GREEN" + (( $(echo "$last < $first" | bc -l 2>/dev/null || echo 0) )) && color="$BR_RED" + + # Print chart + for line in "${chart[@]}"; do + printf " ${color}%s${RST}\n" "$line" + done +} + +render_mini_sparkline() { + local data_str="$1" + local width="${2:-15}" + + # Sparkline characters: ▁▂▃▄▅▆▇█ + local chars=("▁" "▂" "▃" "▄" "▅" "▆" "▇" "█") + + if command -v jq &>/dev/null; then + local -a values=($(echo "$data_str" | jq -r '.[]' 2>/dev/null | tail -n "$width")) + + [[ ${#values[@]} -eq 0 ]] && return + + local min="${values[0]}" + local max="${values[0]}" + for val in "${values[@]}"; do + (( $(echo "$val < $min" | bc -l 2>/dev/null || echo 0) )) && min="$val" + (( $(echo "$val > $max" | bc -l 2>/dev/null || echo 0) )) && max="$val" + done + + local range=$(echo "$max - $min" | bc -l 2>/dev/null || echo 1) + [[ "$range" == "0" ]] && range=1 + + local first="${values[0]}" + local last="${values[-1]}" + local color="$BR_GREEN" + (( $(echo "$last < $first" | bc -l 2>/dev/null || echo 0) )) && color="$BR_RED" + + printf "%s" "$color" + for val in "${values[@]}"; do + local idx=$(echo "($val - $min) * 7 / $range" | bc 2>/dev/null || echo 0) + [[ $idx -gt 7 ]] && idx=7 + [[ $idx -lt 0 ]] && idx=0 + printf "%s" "${chars[$idx]}" + done + printf "${RST}" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# CANDLESTICK CHART (ASCII) +#─────────────────────────────────────────────────────────────────────────────── + +render_candlestick() { + local coin="$1" + local days="${2:-7}" + local width=60 + local height=15 + + local chart_data=$(fetch_market_chart "$coin" "$days") + + if ! command -v jq &>/dev/null; then + printf "${TEXT_MUTED}jq required for chart rendering${RST}\n" + return + fi + + local -a prices=($(echo "$chart_data" | jq -r '.prices[][1]' 2>/dev/null)) + + [[ ${#prices[@]} -eq 0 ]] && return + + local min="${prices[0]}" + local max="${prices[0]}" + for val in "${prices[@]}"; do + (( $(echo "$val < $min" | bc -l 2>/dev/null || echo 0) )) && min="$val" + (( $(echo "$val > $max" | bc -l 2>/dev/null || echo 0) )) && max="$val" + done + + printf "\n${BOLD}%s Price Chart (%d days)${RST}\n\n" "${coin^}" "$days" + printf " ${TEXT_MUTED}Max: \$%s${RST}\n" "$(format_price "$max")" + + render_sparkline "${prices[@]}" + + printf " ${TEXT_MUTED}Min: \$%s${RST}\n" "$(format_price "$min")" +} + +#─────────────────────────────────────────────────────────────────────────────── +# PORTFOLIO MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +init_portfolio() { + if [[ ! -f "$PORTFOLIO_FILE" ]]; then + echo '{"holdings":[]}' > "$PORTFOLIO_FILE" + fi +} + +add_holding() { + local coin="$1" + local amount="$2" + local buy_price="$3" + + init_portfolio + + if command -v jq &>/dev/null; then + local holding="{\"coin\":\"$coin\",\"amount\":$amount,\"buy_price\":$buy_price,\"date\":\"$(date -Iseconds)\"}" + local updated=$(jq ".holdings += [$holding]" "$PORTFOLIO_FILE") + echo "$updated" > "$PORTFOLIO_FILE" + printf "${BR_GREEN}Added %.4f %s at \$%s${RST}\n" "$amount" "$coin" "$buy_price" + fi +} + +remove_holding() { + local index="$1" + + if command -v jq &>/dev/null; then + local updated=$(jq "del(.holdings[$index])" "$PORTFOLIO_FILE") + echo "$updated" > "$PORTFOLIO_FILE" + printf "${BR_YELLOW}Removed holding at index %d${RST}\n" "$index" + fi +} + +get_portfolio_value() { + init_portfolio + + if ! command -v jq &>/dev/null; then + echo "0" + return + fi + + local total=0 + local holdings=$(jq -r '.holdings[] | "\(.coin)|\(.amount)|\(.buy_price)"' "$PORTFOLIO_FILE" 2>/dev/null) + + while IFS='|' read -r coin amount buy_price; do + [[ -z "$coin" ]] && continue + + local price_data=$(fetch_price "$coin") + IFS='|' read -r price change vol mcap <<< "$(parse_price "$price_data" "$coin")" + + local value=$(echo "$amount * $price" | bc -l 2>/dev/null || echo 0) + total=$(echo "$total + $value" | bc -l 2>/dev/null || echo "$total") + done <<< "$holdings" + + printf "%.2f" "$total" +} + +render_portfolio() { + init_portfolio + + printf "${BOLD}Portfolio Holdings${RST}\n\n" + + if ! command -v jq &>/dev/null; then + printf "${TEXT_MUTED}jq required for portfolio${RST}\n" + return + fi + + printf "%-4s %-12s %-12s %-12s %-12s %-12s %-10s\n" "#" "COIN" "AMOUNT" "BUY PRICE" "CURR PRICE" "VALUE" "P/L %" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + + local total_value=0 + local total_invested=0 + local idx=0 + + while IFS='|' read -r coin amount buy_price; do + [[ -z "$coin" ]] && continue + + local price_data=$(fetch_price "$coin") + IFS='|' read -r price change vol mcap <<< "$(parse_price "$price_data" "$coin")" + + local value=$(echo "$amount * $price" | bc -l 2>/dev/null || echo 0) + local invested=$(echo "$amount * $buy_price" | bc -l 2>/dev/null || echo 0) + local pl_pct=$(echo "($price - $buy_price) / $buy_price * 100" | bc -l 2>/dev/null || echo 0) + + local pl_color="$BR_GREEN" + (( $(echo "$pl_pct < 0" | bc -l 2>/dev/null || echo 0) )) && pl_color="$BR_RED" + + printf "${BR_CYAN}%-4d${RST} " "$idx" + printf "${BOLD}%-12s${RST} " "${coin:0:12}" + printf "%-12s " "$(printf "%.4f" "$amount")" + printf "${TEXT_MUTED}\$%-11s${RST} " "$(format_price "$buy_price")" + printf "\$%-11s " "$(format_price "$price")" + printf "${BR_YELLOW}\$%-11s${RST} " "$(format_price "$value")" + printf "${pl_color}%+.2f%%${RST}\n" "$pl_pct" + + total_value=$(echo "$total_value + $value" | bc -l) + total_invested=$(echo "$total_invested + $invested" | bc -l) + ((idx++)) + done < <(jq -r '.holdings[] | "\(.coin)|\(.amount)|\(.buy_price)"' "$PORTFOLIO_FILE" 2>/dev/null) + + if [[ $idx -gt 0 ]]; then + local total_pl=$(echo "($total_value - $total_invested) / $total_invested * 100" | bc -l 2>/dev/null || echo 0) + local pl_color="$BR_GREEN" + (( $(echo "$total_pl < 0" | bc -l 2>/dev/null || echo 0) )) && pl_color="$BR_RED" + + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + printf "${BOLD}TOTAL${RST}%44s ${BR_YELLOW}\$%.2f${RST} ${pl_color}%+.2f%%${RST}\n" "" "$total_value" "$total_pl" + else + printf "${TEXT_MUTED}No holdings. Use 'add' to add coins to portfolio.${RST}\n" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# PRICE ALERTS +#─────────────────────────────────────────────────────────────────────────────── + +init_alerts() { + if [[ ! -f "$ALERTS_FILE" ]]; then + echo '{"alerts":[]}' > "$ALERTS_FILE" + fi +} + +add_alert() { + local coin="$1" + local condition="$2" # above, below + local price="$3" + + init_alerts + + if command -v jq &>/dev/null; then + local alert="{\"coin\":\"$coin\",\"condition\":\"$condition\",\"price\":$price,\"active\":true}" + local updated=$(jq ".alerts += [$alert]" "$ALERTS_FILE") + echo "$updated" > "$ALERTS_FILE" + printf "${BR_GREEN}Alert set: %s %s \$%s${RST}\n" "$coin" "$condition" "$price" + fi +} + +check_alerts() { + init_alerts + + if ! command -v jq &>/dev/null; then + return + fi + + local triggered=() + + while IFS='|' read -r coin condition target_price active; do + [[ "$active" != "true" ]] && continue + + local price_data=$(fetch_price "$coin") + IFS='|' read -r current_price change vol mcap <<< "$(parse_price "$price_data" "$coin")" + + local triggered_flag=false + + if [[ "$condition" == "above" ]]; then + (( $(echo "$current_price >= $target_price" | bc -l 2>/dev/null || echo 0) )) && triggered_flag=true + elif [[ "$condition" == "below" ]]; then + (( $(echo "$current_price <= $target_price" | bc -l 2>/dev/null || echo 0) )) && triggered_flag=true + fi + + if [[ "$triggered_flag" == "true" ]]; then + printf "${BR_YELLOW}⚠ ALERT: %s is %s \$%s (target: \$%s)${RST}\n" "$coin" "$condition" "$current_price" "$target_price" + fi + done < <(jq -r '.alerts[] | "\(.coin)|\(.condition)|\(.price)|\(.active)"' "$ALERTS_FILE" 2>/dev/null) +} + +#─────────────────────────────────────────────────────────────────────────────── +# MARKET OVERVIEW +#─────────────────────────────────────────────────────────────────────────────── + +render_market_overview() { + local global=$(fetch_global_data) + + if ! command -v jq &>/dev/null; then + printf "${TEXT_MUTED}jq required for market overview${RST}\n" + return + fi + + local total_mcap=$(echo "$global" | jq -r '.data.total_market_cap.usd // 0' 2>/dev/null) + local total_vol=$(echo "$global" | jq -r '.data.total_volume.usd // 0' 2>/dev/null) + local btc_dom=$(echo "$global" | jq -r '.data.market_cap_percentage.btc // 0' 2>/dev/null) + local eth_dom=$(echo "$global" | jq -r '.data.market_cap_percentage.eth // 0' 2>/dev/null) + local active_coins=$(echo "$global" | jq -r '.data.active_cryptocurrencies // 0' 2>/dev/null) + local mcap_change=$(echo "$global" | jq -r '.data.market_cap_change_percentage_24h_usd // 0' 2>/dev/null) + + local change_color="$BR_GREEN" + (( $(echo "$mcap_change < 0" | bc -l 2>/dev/null || echo 0) )) && change_color="$BR_RED" + + printf " ${BOLD}Total Market Cap:${RST} ${BR_CYAN}\$%s${RST} " "$(format_large_number "$total_mcap")" + printf "${change_color}(%+.2f%%)${RST}\n" "$mcap_change" + + printf " ${BOLD}24h Volume:${RST} ${BR_YELLOW}\$%s${RST} " "$(format_large_number "$total_vol")" + printf "${BOLD}Active Coins:${RST} ${TEXT_SECONDARY}%s${RST}\n" "$active_coins" + + printf " ${BOLD}BTC Dominance:${RST} " + render_dominance_bar "$btc_dom" "$BR_ORANGE" + printf " ${BOLD}ETH Dominance:${RST} " + render_dominance_bar "$eth_dom" "$BR_PURPLE" +} + +render_dominance_bar() { + local pct="$1" + local color="$2" + local width=20 + + local filled=$(printf "%.0f" "$(echo "$pct * $width / 100" | bc -l 2>/dev/null || echo 0)") + local empty=$((width - filled)) + + printf "%s" "$color" + printf "%0.s█" $(seq 1 $filled 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $empty 2>/dev/null) || true + printf "${RST} %.1f%%\n" "$pct" +} + +#─────────────────────────────────────────────────────────────────────────────── +# TOP COINS LIST +#─────────────────────────────────────────────────────────────────────────────── + +render_top_coins() { + local count="${1:-15}" + local data=$(fetch_top_coins "$count") + + if ! command -v jq &>/dev/null; then + printf "${TEXT_MUTED}jq required${RST}\n" + return + fi + + printf "\n${BOLD}Top %d Cryptocurrencies${RST}\n\n" "$count" + + printf "%-4s %-15s %-12s %-10s %-14s %-15s\n" "#" "COIN" "PRICE" "24h %" "MARKET CAP" "SPARKLINE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + + echo "$data" | jq -c '.[]' 2>/dev/null | while read -r coin; do + local rank=$(echo "$coin" | jq -r '.market_cap_rank') + local name=$(echo "$coin" | jq -r '.symbol' | tr '[:lower:]' '[:upper:]') + local price=$(echo "$coin" | jq -r '.current_price') + local change=$(echo "$coin" | jq -r '.price_change_percentage_24h // 0') + local mcap=$(echo "$coin" | jq -r '.market_cap') + local sparkline=$(echo "$coin" | jq -c '.sparkline_in_7d.price // []') + + local change_color="$BR_GREEN" + (( $(echo "$change < 0" | bc -l 2>/dev/null || echo 0) )) && change_color="$BR_RED" + + printf "${BR_CYAN}%-4s${RST} " "$rank" + printf "${BOLD}%-15s${RST} " "$name" + printf "\$%-11s " "$(format_price "$price")" + printf "${change_color}%+8.2f%%${RST} " "$change" + printf "${TEXT_MUTED}\$%-12s${RST} " "$(format_large_number "$mcap")" + render_mini_sparkline "$sparkline" 12 + printf "\n" + done | head -n "$count" +} + +#─────────────────────────────────────────────────────────────────────────────── +# WATCHLIST +#─────────────────────────────────────────────────────────────────────────────── + +render_watchlist() { + local -a coins=("${@:-${DEFAULT_COINS[@]}}") + + printf "${BOLD}Watchlist${RST}\n\n" + + for coin in "${coins[@]}"; do + local price_data=$(fetch_price "$coin") + IFS='|' read -r price change vol mcap <<< "$(parse_price "$price_data" "$coin")" + + local change_color="$BR_GREEN" + local arrow="↑" + if (( $(echo "$change < 0" | bc -l 2>/dev/null || echo 0) )); then + change_color="$BR_RED" + arrow="↓" + fi + + printf " ${BOLD}%-12s${RST} " "${coin^^}" + printf "${BR_CYAN}\$%-12s${RST} " "$(format_price "$price")" + printf "${change_color}%s %+.2f%%${RST} " "$arrow" "$change" + printf "${TEXT_MUTED}Vol: \$%s${RST}\n" "$(format_large_number "$vol")" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_crypto_dashboard() { + clear_screen + cursor_hide + + printf "${BR_YELLOW}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 💰 CRYPTO TRADING DASHBOARD ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Market overview + render_market_overview + printf "\n" + + # Two-column layout + printf "${BR_CYAN}┌───────────────────────────────────────┬──────────────────────────────────────┐${RST}\n" + printf "${BR_CYAN}│${RST} ${BOLD}BTC/USD${RST} ${BR_CYAN}│${RST} ${BOLD}ETH/USD${RST} ${BR_CYAN}│${RST}\n" + + # Fetch BTC and ETH + local btc_data=$(fetch_price "bitcoin") + local eth_data=$(fetch_price "ethereum") + + IFS='|' read -r btc_price btc_change btc_vol btc_mcap <<< "$(parse_price "$btc_data" "bitcoin")" + IFS='|' read -r eth_price eth_change eth_vol eth_mcap <<< "$(parse_price "$eth_data" "ethereum")" + + local btc_color="$BR_GREEN" + (( $(echo "$btc_change < 0" | bc -l 2>/dev/null || echo 0) )) && btc_color="$BR_RED" + + local eth_color="$BR_GREEN" + (( $(echo "$eth_change < 0" | bc -l 2>/dev/null || echo 0) )) && eth_color="$BR_RED" + + printf "${BR_CYAN}│${RST} ${BR_ORANGE}\$%-36s${RST}${BR_CYAN}│${RST} ${BR_PURPLE}\$%-35s${RST}${BR_CYAN}│${RST}\n" \ + "$(format_price "$btc_price")" "$(format_price "$eth_price")" + + printf "${BR_CYAN}│${RST} ${btc_color}%+.2f%%${RST}%30s${BR_CYAN}│${RST} ${eth_color}%+.2f%%${RST}%29s${BR_CYAN}│${RST}\n" \ + "$btc_change" "" "$eth_change" "" + + printf "${BR_CYAN}├───────────────────────────────────────┴──────────────────────────────────────┤${RST}\n" + + # Top coins + printf "${BR_CYAN}│${RST} ${BOLD}Top Cryptocurrencies${RST}%56s${BR_CYAN}│${RST}\n" "" + + local top_data=$(fetch_top_coins 8) + if command -v jq &>/dev/null; then + echo "$top_data" | jq -c '.[]' 2>/dev/null | head -8 | while read -r coin; do + local rank=$(echo "$coin" | jq -r '.market_cap_rank') + local symbol=$(echo "$coin" | jq -r '.symbol' | tr '[:lower:]' '[:upper:]') + local price=$(echo "$coin" | jq -r '.current_price') + local change=$(echo "$coin" | jq -r '.price_change_percentage_24h // 0') + + local change_color="$BR_GREEN" + (( $(echo "$change < 0" | bc -l 2>/dev/null || echo 0) )) && change_color="$BR_RED" + + printf "${BR_CYAN}│${RST} ${TEXT_MUTED}%2d${RST} ${BOLD}%-8s${RST} \$%-15s ${change_color}%+8.2f%%${RST}%33s${BR_CYAN}│${RST}\n" \ + "$rank" "$symbol" "$(format_price "$price")" "$change" "" + done + fi + + printf "${BR_CYAN}└──────────────────────────────────────────────────────────────────────────────┘${RST}\n" + + # Check alerts + check_alerts + + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[t]op coins [c]hart [p]ortfolio [a]lert [w]atchlist [r]efresh [q]uit${RST}\n" +} + +crypto_dashboard_loop() { + while true; do + render_crypto_dashboard + + if read -rsn1 -t 5 key 2>/dev/null; then + case "$key" in + t|T) + clear_screen + render_top_coins 20 + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + c|C) + printf "\n${BR_CYAN}Coin (e.g., bitcoin): ${RST}" + cursor_show + read -r coin + cursor_hide + [[ -n "$coin" ]] && render_candlestick "$coin" 7 + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + p|P) + clear_screen + render_portfolio + printf "\n${TEXT_SECONDARY}[a]dd [r]emove [b]ack${RST}\n" + read -rsn1 pkey + case "$pkey" in + a|A) + printf "${BR_CYAN}Coin: ${RST}" + cursor_show + read -r coin + printf "${BR_CYAN}Amount: ${RST}" + read -r amount + printf "${BR_CYAN}Buy price: ${RST}" + read -r price + cursor_hide + add_holding "$coin" "$amount" "$price" + sleep 1 + ;; + r|R) + printf "${BR_CYAN}Index to remove: ${RST}" + cursor_show + read -r idx + cursor_hide + remove_holding "$idx" + sleep 1 + ;; + esac + ;; + a|A) + printf "\n${BR_CYAN}Coin: ${RST}" + cursor_show + read -r coin + printf "${BR_CYAN}Condition (above/below): ${RST}" + read -r condition + printf "${BR_CYAN}Price: ${RST}" + read -r price + cursor_hide + add_alert "$coin" "$condition" "$price" + sleep 1 + ;; + w|W) + clear_screen + render_watchlist + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + r|R) continue ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) crypto_dashboard_loop ;; + price) fetch_price "$2" | jq '.' 2>/dev/null ;; + top) render_top_coins "${2:-20}" ;; + chart) render_candlestick "$2" "${3:-7}" ;; + watchlist) render_watchlist "${@:2}" ;; + portfolio) render_portfolio ;; + add) add_holding "$2" "$3" "$4" ;; + alert) add_alert "$2" "$3" "$4" ;; + market) render_market_overview ;; + *) + printf "Usage: %s [dashboard|price|top|chart|watchlist|portfolio|add|alert|market]\n" "$0" + ;; + esac +fi diff --git a/data-export.sh b/data-export.sh new file mode 100644 index 0000000..6b1b992 --- /dev/null +++ b/data-export.sh @@ -0,0 +1,600 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███████╗██╗ ██╗██████╗ ██████╗ ██████╗ ████████╗ +# ██╔════╝╚██╗██╔╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝ +# █████╗ ╚███╔╝ ██████╔╝██║ ██║██████╔╝ ██║ +# ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║██╔══██╗ ██║ +# ███████╗██╔╝ ██╗██║ ╚██████╔╝██║ ██║ ██║ +# ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD DATA EXPORT SYSTEM v2.0 +# Export dashboard data to JSON, CSV, HTML, and Markdown +#═══════════════════════════════════════════════════════════════════════════════ + +# Source core library +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +EXPORT_DIR="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards/exports}" +mkdir -p "$EXPORT_DIR" 2>/dev/null + +# Supported formats +EXPORT_FORMATS=("json" "csv" "html" "md" "txt") + +#─────────────────────────────────────────────────────────────────────────────── +# CORE EXPORT FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Generate export filename +generate_filename() { + local prefix="${1:-export}" + local format="${2:-json}" + local timestamp=$(date "+%Y%m%d_%H%M%S") + + echo "${EXPORT_DIR}/${prefix}_${timestamp}.${format}" +} + +# Export to JSON +export_json() { + local data="$1" + local filename="${2:-$(generate_filename "data" "json")}" + + # Validate JSON if jq available + if command -v jq &>/dev/null; then + echo "$data" | jq '.' > "$filename" 2>/dev/null || { + # If invalid JSON, wrap as raw + jq -n --arg raw "$data" '{data: $raw, exported_at: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))}' > "$filename" + } + else + echo "$data" > "$filename" + fi + + log_info "Exported to JSON: $filename" + echo "$filename" +} + +# Export to CSV +export_csv() { + local json_data="$1" + local filename="${2:-$(generate_filename "data" "csv")}" + + if ! command -v jq &>/dev/null; then + log_error "jq is required for CSV export" + return 1 + fi + + # Detect if array or object + local data_type=$(echo "$json_data" | jq -r 'type' 2>/dev/null) + + case "$data_type" in + array) + # Get headers from first object + local headers=$(echo "$json_data" | jq -r '.[0] | keys_unsorted | @csv' 2>/dev/null) + + if [[ -n "$headers" ]]; then + echo "$headers" > "$filename" + echo "$json_data" | jq -r '.[] | [.[]] | @csv' >> "$filename" 2>/dev/null + else + # Simple array + echo "value" > "$filename" + echo "$json_data" | jq -r '.[]' >> "$filename" + fi + ;; + object) + # Single object - key,value format + echo "key,value" > "$filename" + echo "$json_data" | jq -r 'to_entries[] | [.key, .value] | @csv' >> "$filename" 2>/dev/null + ;; + *) + echo "data" > "$filename" + echo "$json_data" >> "$filename" + ;; + esac + + log_info "Exported to CSV: $filename" + echo "$filename" +} + +# Export to HTML +export_html() { + local title="$1" + local data="$2" + local filename="${3:-$(generate_filename "report" "html")}" + + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + + cat > "$filename" << 'HTMLHEAD' +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>BlackRoad Dashboard Export + + + +
+
+

BLACKROAD DASHBOARD

+HTMLHEAD + + printf '

%s | Generated: %s

\n' "$title" "$timestamp" >> "$filename" + echo '
' >> "$filename" + echo '' >> "$filename" + + # Add data section + echo '
' >> "$filename" + echo '

Data Export

' >> "$filename" + echo '
' >> "$filename"
+
+    # Format data based on type
+    if command -v jq &>/dev/null && echo "$data" | jq '.' &>/dev/null; then
+        echo "$data" | jq '.' >> "$filename"
+    else
+        echo "$data" >> "$filename"
+    fi
+
+    cat >> "$filename" << 'HTMLFOOT'
+            
+
+ +
+

Exported by BlackRoad Dashboards | GitHub

+
+
+ + +HTMLFOOT + + log_info "Exported to HTML: $filename" + echo "$filename" +} + +# Export to Markdown +export_markdown() { + local title="$1" + local data="$2" + local filename="${3:-$(generate_filename "report" "md")}" + + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + + cat > "$filename" << EOF +# $title + +> Generated: $timestamp + +--- + +## Data + +\`\`\`json +$(echo "$data" | jq '.' 2>/dev/null || echo "$data") +\`\`\` + +--- + +*Exported by BlackRoad Dashboards* +EOF + + log_info "Exported to Markdown: $filename" + echo "$filename" +} + +# Export to plain text +export_txt() { + local title="$1" + local data="$2" + local filename="${3:-$(generate_filename "report" "txt")}" + + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + + cat > "$filename" << EOF +================================================================================ + $title + Generated: $timestamp +================================================================================ + +$data + +================================================================================ + Exported by BlackRoad Dashboards +================================================================================ +EOF + + log_info "Exported to TXT: $filename" + echo "$filename" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DATA COLLECTORS +#─────────────────────────────────────────────────────────────────────────────── + +# Collect system metrics +collect_system_metrics() { + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + local uptime=$(get_uptime_seconds 2>/dev/null || echo "0") + + cat << EOF +{ + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "system": { + "cpu_usage_percent": $cpu, + "memory_usage_percent": $mem, + "disk_usage_percent": $disk, + "uptime_seconds": $uptime, + "hostname": "$(hostname)", + "os": "$(uname -s)", + "kernel": "$(uname -r)" + } +} +EOF +} + +# Collect network status +collect_network_status() { + local devices=( + '{"name":"Lucidia Prime","host":"192.168.4.38"}' + '{"name":"BlackRoad Pi","host":"192.168.4.64"}' + '{"name":"Lucidia Alt","host":"192.168.4.99"}' + '{"name":"iPhone Koder","host":"192.168.4.68"}' + '{"name":"Codex Infinity","host":"159.65.43.12"}' + ) + + printf '{"timestamp":"%s","devices":[' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + local first=true + for device_json in "${devices[@]}"; do + local name=$(echo "$device_json" | jq -r '.name') + local host=$(echo "$device_json" | jq -r '.host') + local status="offline" + local latency="null" + + if ping -c 1 -W 2 "$host" &>/dev/null; then + status="online" + latency=$(ping -c 1 "$host" 2>/dev/null | grep -oE 'time=[0-9.]+' | cut -d= -f2) + fi + + [[ "$first" != "true" ]] && printf "," + first=false + + printf '{"name":"%s","host":"%s","status":"%s","latency_ms":%s}' \ + "$name" "$host" "$status" "${latency:-null}" + done + + printf ']}' +} + +# Collect crypto prices +collect_crypto_prices() { + local btc=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local eth=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local sol=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + + cat << EOF +{ + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "prices": { + "bitcoin": $(echo "$btc" | jq '.bitcoin' 2>/dev/null || echo "null"), + "ethereum": $(echo "$eth" | jq '.ethereum' 2>/dev/null || echo "null"), + "solana": $(echo "$sol" | jq '.solana' 2>/dev/null || echo "null") + } +} +EOF +} + +# Collect full dashboard snapshot +collect_dashboard_snapshot() { + local system=$(collect_system_metrics) + local network=$(collect_network_status) + local crypto=$(collect_crypto_prices) + + cat << EOF +{ + "snapshot_id": "$(uuidgen 2>/dev/null || echo $$-$(date +%s))", + "generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "system_metrics": $(echo "$system" | jq '.system' 2>/dev/null || echo "{}"), + "network_status": $(echo "$network" | jq '.devices' 2>/dev/null || echo "[]"), + "crypto_prices": $(echo "$crypto" | jq '.prices' 2>/dev/null || echo "{}") +} +EOF +} + +#─────────────────────────────────────────────────────────────────────────────── +# EXPORT UI +#─────────────────────────────────────────────────────────────────────────────── + +export_ui() { + clear_screen + cursor_hide + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 📤 BLACKROAD DATA EXPORT ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + printf "${TEXT_SECONDARY}Select data to export:${RST}\n\n" + printf " ${BR_ORANGE}1.${RST} System Metrics\n" + printf " ${BR_ORANGE}2.${RST} Network Status\n" + printf " ${BR_ORANGE}3.${RST} Crypto Prices\n" + printf " ${BR_ORANGE}4.${RST} Full Dashboard Snapshot\n" + printf " ${BR_ORANGE}5.${RST} Custom JSON Data\n" + printf "\n" + + read -p "$(printf "${BR_CYAN}Enter choice [1-5]: ${RST}")" data_choice + + local data="" + local title="" + + case "$data_choice" in + 1) data=$(collect_system_metrics); title="System Metrics" ;; + 2) data=$(collect_network_status); title="Network Status" ;; + 3) data=$(collect_crypto_prices); title="Crypto Prices" ;; + 4) data=$(collect_dashboard_snapshot); title="Dashboard Snapshot" ;; + 5) + printf "\n${TEXT_SECONDARY}Enter JSON data (end with empty line):${RST}\n" + local input="" + while IFS= read -r line; do + [[ -z "$line" ]] && break + input+="$line" + done + data="$input" + title="Custom Export" + ;; + *) log_error "Invalid choice"; return 1 ;; + esac + + printf "\n${TEXT_SECONDARY}Select export format:${RST}\n\n" + printf " ${BR_GREEN}1.${RST} JSON\n" + printf " ${BR_GREEN}2.${RST} CSV\n" + printf " ${BR_GREEN}3.${RST} HTML Report\n" + printf " ${BR_GREEN}4.${RST} Markdown\n" + printf " ${BR_GREEN}5.${RST} Plain Text\n" + printf " ${BR_GREEN}6.${RST} All Formats\n" + printf "\n" + + read -p "$(printf "${BR_CYAN}Enter choice [1-6]: ${RST}")" format_choice + + local exported_files=() + + case "$format_choice" in + 1) exported_files+=("$(export_json "$data")") ;; + 2) exported_files+=("$(export_csv "$data")") ;; + 3) exported_files+=("$(export_html "$title" "$data")") ;; + 4) exported_files+=("$(export_markdown "$title" "$data")") ;; + 5) exported_files+=("$(export_txt "$title" "$data")") ;; + 6) + exported_files+=("$(export_json "$data")") + exported_files+=("$(export_csv "$data")") + exported_files+=("$(export_html "$title" "$data")") + exported_files+=("$(export_markdown "$title" "$data")") + exported_files+=("$(export_txt "$title" "$data")") + ;; + *) log_error "Invalid format"; return 1 ;; + esac + + printf "\n${BR_GREEN}${BOLD}Export Complete!${RST}\n\n" + printf "${TEXT_SECONDARY}Files created:${RST}\n" + for file in "${exported_files[@]}"; do + printf " ${BR_CYAN}→${RST} %s\n" "$file" + done + + printf "\n${TEXT_MUTED}Press any key to continue...${RST}" + read -rsn1 + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# SCHEDULED EXPORTS +#─────────────────────────────────────────────────────────────────────────────── + +# Run scheduled export (for cron jobs) +scheduled_export() { + local export_type="${1:-snapshot}" + local format="${2:-json}" + + case "$export_type" in + system) data=$(collect_system_metrics); title="System Metrics" ;; + network) data=$(collect_network_status); title="Network Status" ;; + crypto) data=$(collect_crypto_prices); title="Crypto Prices" ;; + snapshot) data=$(collect_dashboard_snapshot); title="Dashboard Snapshot" ;; + esac + + case "$format" in + json) export_json "$data" ;; + csv) export_csv "$data" ;; + html) export_html "$title" "$data" ;; + md) export_markdown "$title" "$data" ;; + txt) export_txt "$title" "$data" ;; + esac +} + +# List recent exports +list_exports() { + printf "${BR_CYAN}${BOLD}Recent Exports:${RST}\n\n" + + if [[ -d "$EXPORT_DIR" ]]; then + ls -lt "$EXPORT_DIR" | head -20 | while read -r line; do + printf " %s\n" "$line" + done + else + printf " ${TEXT_MUTED}No exports found.${RST}\n" + fi +} + +# Clean old exports +clean_exports() { + local days="${1:-7}" + + find "$EXPORT_DIR" -type f -mtime "+$days" -delete 2>/dev/null + log_info "Cleaned exports older than $days days" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-ui}" in + ui) export_ui ;; + json) export_json "${2:-$(collect_dashboard_snapshot)}" "${3:-}" ;; + csv) export_csv "${2:-$(collect_dashboard_snapshot)}" "${3:-}" ;; + html) export_html "${2:-Dashboard}" "${3:-$(collect_dashboard_snapshot)}" "${4:-}" ;; + md) export_markdown "${2:-Dashboard}" "${3:-$(collect_dashboard_snapshot)}" "${4:-}" ;; + txt) export_txt "${2:-Dashboard}" "${3:-$(collect_dashboard_snapshot)}" "${4:-}" ;; + scheduled) scheduled_export "$2" "$3" ;; + list) list_exports ;; + clean) clean_exports "${2:-7}" ;; + snapshot) collect_dashboard_snapshot ;; + system) collect_system_metrics ;; + network) collect_network_status ;; + crypto) collect_crypto_prices ;; + *) + printf "Usage: %s [ui|json|csv|html|md|txt|scheduled|list|clean|snapshot]\n" "$0" + printf " %s scheduled snapshot json # For cron jobs\n" "$0" + printf " %s clean 30 # Remove exports older than 30 days\n" "$0" + ;; + esac +fi diff --git a/database-orm.sh b/database-orm.sh new file mode 100644 index 0000000..631716b --- /dev/null +++ b/database-orm.sh @@ -0,0 +1,798 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ █████╗ ████████╗ █████╗ ██████╗ █████╗ ███████╗███████╗ +# ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔════╝ +# ██║ ██║███████║ ██║ ███████║██████╔╝███████║███████╗█████╗ +# ██║ ██║██╔══██║ ██║ ██╔══██║██╔══██╗██╔══██║╚════██║██╔══╝ +# ██████╔╝██║ ██║ ██║ ██║ ██║██████╔╝██║ ██║███████║███████╗ +# ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD DATABASE ORM v3.0 +# SQLite Object-Relational Mapping & Query Builder for Bash +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +DB_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/db" +DEFAULT_DB="$DB_DIR/blackroad.db" + +mkdir -p "$DB_DIR" 2>/dev/null + +# Current database context +CURRENT_DB="$DEFAULT_DB" +CURRENT_TABLE="" + +#─────────────────────────────────────────────────────────────────────────────── +# DATABASE CONNECTION +#─────────────────────────────────────────────────────────────────────────────── + +check_sqlite() { + if ! command -v sqlite3 &>/dev/null; then + printf "${BR_RED}SQLite3 is not installed!${RST}\n" + return 1 + fi + return 0 +} + +db_connect() { + local db_path="${1:-$DEFAULT_DB}" + CURRENT_DB="$db_path" + + if [[ ! -f "$CURRENT_DB" ]]; then + touch "$CURRENT_DB" + fi +} + +db_close() { + CURRENT_DB="" + CURRENT_TABLE="" +} + +db_query() { + local query="$1" + local mode="${2:--separator |}" + + sqlite3 $mode "$CURRENT_DB" "$query" 2>&1 +} + +db_exec() { + local query="$1" + sqlite3 "$CURRENT_DB" "$query" 2>&1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# SCHEMA MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +# Create table with schema +db_create_table() { + local table_name="$1" + shift + local columns=("$@") + + local schema="CREATE TABLE IF NOT EXISTS $table_name (" + schema+="id INTEGER PRIMARY KEY AUTOINCREMENT," + + for col in "${columns[@]}"; do + schema+="$col," + done + + schema+="created_at DATETIME DEFAULT CURRENT_TIMESTAMP," + schema+="updated_at DATETIME DEFAULT CURRENT_TIMESTAMP" + schema+=");" + + db_exec "$schema" + + # Create trigger for updated_at + local trigger="CREATE TRIGGER IF NOT EXISTS ${table_name}_updated_at + AFTER UPDATE ON $table_name + FOR EACH ROW + BEGIN + UPDATE $table_name SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id; + END;" + + db_exec "$trigger" +} + +# Drop table +db_drop_table() { + local table_name="$1" + db_exec "DROP TABLE IF EXISTS $table_name;" +} + +# List tables +db_list_tables() { + db_query "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" "-list" +} + +# Get table info +db_table_info() { + local table_name="$1" + db_query "PRAGMA table_info($table_name);" +} + +# Check if table exists +db_table_exists() { + local table_name="$1" + local result=$(db_query "SELECT name FROM sqlite_master WHERE type='table' AND name='$table_name';" "-list") + [[ -n "$result" ]] +} + +#─────────────────────────────────────────────────────────────────────────────── +# ORM MODEL +#─────────────────────────────────────────────────────────────────────────────── + +# Set current table context +model() { + local table_name="$1" + CURRENT_TABLE="$table_name" +} + +# Create new record +orm_create() { + local table="${1:-$CURRENT_TABLE}" + shift + + local columns="" + local values="" + + while [[ $# -gt 0 ]]; do + local key="${1%%=*}" + local value="${1#*=}" + + [[ -n "$columns" ]] && columns+="," + [[ -n "$values" ]] && values+="," + + columns+="$key" + values+="'${value//\'/\'\'}'" + + shift + done + + local query="INSERT INTO $table ($columns) VALUES ($values);" + db_exec "$query" + + # Return last insert id + db_query "SELECT last_insert_rowid();" "-list" +} + +# Find by ID +orm_find() { + local table="${1:-$CURRENT_TABLE}" + local id="$2" + + db_query "SELECT * FROM $table WHERE id = $id LIMIT 1;" +} + +# Find all records +orm_all() { + local table="${1:-$CURRENT_TABLE}" + local limit="${2:-100}" + local offset="${3:-0}" + + db_query "SELECT * FROM $table LIMIT $limit OFFSET $offset;" +} + +# Find with conditions +orm_where() { + local table="${1:-$CURRENT_TABLE}" + shift + local conditions="$*" + + db_query "SELECT * FROM $table WHERE $conditions;" +} + +# Update record +orm_update() { + local table="${1:-$CURRENT_TABLE}" + local id="$2" + shift 2 + + local set_clause="" + + while [[ $# -gt 0 ]]; do + local key="${1%%=*}" + local value="${1#*=}" + + [[ -n "$set_clause" ]] && set_clause+="," + set_clause+="$key='${value//\'/\'\'}'" + + shift + done + + db_exec "UPDATE $table SET $set_clause WHERE id = $id;" +} + +# Delete record +orm_delete() { + local table="${1:-$CURRENT_TABLE}" + local id="$2" + + db_exec "DELETE FROM $table WHERE id = $id;" +} + +# Count records +orm_count() { + local table="${1:-$CURRENT_TABLE}" + local conditions="${2:-1=1}" + + db_query "SELECT COUNT(*) FROM $table WHERE $conditions;" "-list" +} + +# Check if record exists +orm_exists() { + local table="${1:-$CURRENT_TABLE}" + local id="$2" + + local count=$(orm_count "$table" "id = $id") + [[ "$count" -gt 0 ]] +} + +#─────────────────────────────────────────────────────────────────────────────── +# QUERY BUILDER +#─────────────────────────────────────────────────────────────────────────────── + +# Query builder state +declare -A QB_STATE + +qb_reset() { + QB_STATE[table]="" + QB_STATE[select]="*" + QB_STATE[where]="" + QB_STATE[order]="" + QB_STATE[limit]="" + QB_STATE[offset]="" + QB_STATE[joins]="" + QB_STATE[group]="" + QB_STATE[having]="" +} + +qb_table() { + QB_STATE[table]="$1" +} + +qb_select() { + QB_STATE[select]="$*" +} + +qb_where() { + local condition="$*" + if [[ -n "${QB_STATE[where]}" ]]; then + QB_STATE[where]+=" AND $condition" + else + QB_STATE[where]="$condition" + fi +} + +qb_or_where() { + local condition="$*" + if [[ -n "${QB_STATE[where]}" ]]; then + QB_STATE[where]+=" OR $condition" + else + QB_STATE[where]="$condition" + fi +} + +qb_order() { + QB_STATE[order]="$*" +} + +qb_limit() { + QB_STATE[limit]="$1" +} + +qb_offset() { + QB_STATE[offset]="$1" +} + +qb_join() { + local type="${1:-INNER}" + local table="$2" + local on="$3" + + QB_STATE[joins]+=" $type JOIN $table ON $on" +} + +qb_group() { + QB_STATE[group]="$*" +} + +qb_having() { + QB_STATE[having]="$*" +} + +qb_build() { + local query="SELECT ${QB_STATE[select]} FROM ${QB_STATE[table]}" + + [[ -n "${QB_STATE[joins]}" ]] && query+="${QB_STATE[joins]}" + [[ -n "${QB_STATE[where]}" ]] && query+=" WHERE ${QB_STATE[where]}" + [[ -n "${QB_STATE[group]}" ]] && query+=" GROUP BY ${QB_STATE[group]}" + [[ -n "${QB_STATE[having]}" ]] && query+=" HAVING ${QB_STATE[having]}" + [[ -n "${QB_STATE[order]}" ]] && query+=" ORDER BY ${QB_STATE[order]}" + [[ -n "${QB_STATE[limit]}" ]] && query+=" LIMIT ${QB_STATE[limit]}" + [[ -n "${QB_STATE[offset]}" ]] && query+=" OFFSET ${QB_STATE[offset]}" + + echo "$query" +} + +qb_get() { + local query=$(qb_build) + db_query "$query" +} + +qb_first() { + QB_STATE[limit]=1 + local query=$(qb_build) + db_query "$query" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MIGRATIONS +#─────────────────────────────────────────────────────────────────────────────── + +MIGRATIONS_TABLE="__migrations" + +init_migrations() { + db_exec "CREATE TABLE IF NOT EXISTS $MIGRATIONS_TABLE ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + batch INTEGER NOT NULL, + executed_at DATETIME DEFAULT CURRENT_TIMESTAMP + );" +} + +run_migration() { + local name="$1" + local up_sql="$2" + local down_sql="$3" + + # Check if already run + local exists=$(db_query "SELECT name FROM $MIGRATIONS_TABLE WHERE name = '$name';" "-list") + if [[ -n "$exists" ]]; then + printf "${TEXT_MUTED}Migration already run: %s${RST}\n" "$name" + return 0 + fi + + # Get batch number + local batch=$(db_query "SELECT COALESCE(MAX(batch), 0) + 1 FROM $MIGRATIONS_TABLE;" "-list") + + # Run migration + printf "${BR_CYAN}Running migration: %s${RST}\n" "$name" + db_exec "$up_sql" + + # Record migration + db_exec "INSERT INTO $MIGRATIONS_TABLE (name, batch) VALUES ('$name', $batch);" + + printf "${BR_GREEN}Migration completed: %s${RST}\n" "$name" +} + +rollback_migration() { + local batch="${1:-}" + + if [[ -z "$batch" ]]; then + batch=$(db_query "SELECT MAX(batch) FROM $MIGRATIONS_TABLE;" "-list") + fi + + local migrations=$(db_query "SELECT name FROM $MIGRATIONS_TABLE WHERE batch = $batch ORDER BY id DESC;" "-list") + + for name in $migrations; do + printf "${BR_YELLOW}Rolling back: %s${RST}\n" "$name" + # Note: Would need to store down_sql or use naming convention + db_exec "DELETE FROM $MIGRATIONS_TABLE WHERE name = '$name';" + done +} + +list_migrations() { + printf "${BOLD}Migrations${RST}\n\n" + printf "%-5s %-40s %-10s %-20s\n" "ID" "NAME" "BATCH" "EXECUTED" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────" + + while IFS='|' read -r id name batch executed; do + printf "%-5s %-40s %-10s %-20s\n" "$id" "$name" "$batch" "$executed" + done < <(db_query "SELECT id, name, batch, executed_at FROM $MIGRATIONS_TABLE ORDER BY id;") +} + +#─────────────────────────────────────────────────────────────────────────────── +# SEEDING +#─────────────────────────────────────────────────────────────────────────────── + +seed_table() { + local table="$1" + local count="${2:-10}" + shift 2 + local columns=("$@") + + printf "${BR_CYAN}Seeding %s with %d records...${RST}\n" "$table" "$count" + + for ((i=1; i<=count; i++)); do + local values=() + + for col in "${columns[@]}"; do + local col_name="${col%%:*}" + local col_type="${col#*:}" + + case "$col_type" in + string) + values+=("$col_name=Seed_${col_name}_$i") + ;; + int|integer) + values+=("$col_name=$((RANDOM % 1000))") + ;; + float) + values+=("$col_name=$((RANDOM % 100)).$((RANDOM % 100))") + ;; + bool|boolean) + values+=("$col_name=$((RANDOM % 2))") + ;; + date) + values+=("$col_name=$(date -d "$((RANDOM % 365)) days ago" +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d)") + ;; + email) + values+=("$col_name=user${i}@example.com") + ;; + *) + values+=("$col_name=$col_type") + ;; + esac + done + + orm_create "$table" "${values[@]}" >/dev/null + done + + printf "${BR_GREEN}Seeded %d records${RST}\n" "$count" +} + +#─────────────────────────────────────────────────────────────────────────────── +# RELATIONSHIPS +#─────────────────────────────────────────────────────────────────────────────── + +# Has many relationship +has_many() { + local parent_table="$1" + local parent_id="$2" + local child_table="$3" + local foreign_key="${4:-${parent_table}_id}" + + db_query "SELECT * FROM $child_table WHERE $foreign_key = $parent_id;" +} + +# Belongs to relationship +belongs_to() { + local child_table="$1" + local child_id="$2" + local parent_table="$3" + local foreign_key="${4:-${parent_table}_id}" + + local parent_id=$(db_query "SELECT $foreign_key FROM $child_table WHERE id = $child_id;" "-list") + orm_find "$parent_table" "$parent_id" +} + +# Many to many relationship +many_to_many() { + local table1="$1" + local id1="$2" + local table2="$3" + local pivot_table="${4:-${table1}_${table2}}" + + db_query "SELECT t2.* FROM $table2 t2 + INNER JOIN $pivot_table p ON p.${table2}_id = t2.id + WHERE p.${table1}_id = $id1;" +} + +#─────────────────────────────────────────────────────────────────────────────── +# TRANSACTIONS +#─────────────────────────────────────────────────────────────────────────────── + +transaction_begin() { + db_exec "BEGIN TRANSACTION;" +} + +transaction_commit() { + db_exec "COMMIT;" +} + +transaction_rollback() { + db_exec "ROLLBACK;" +} + +# Run queries in transaction +with_transaction() { + local callback="$1" + + transaction_begin + if $callback; then + transaction_commit + return 0 + else + transaction_rollback + return 1 + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# AGGREGATIONS +#─────────────────────────────────────────────────────────────────────────────── + +db_sum() { + local table="$1" + local column="$2" + local conditions="${3:-1=1}" + + db_query "SELECT SUM($column) FROM $table WHERE $conditions;" "-list" +} + +db_avg() { + local table="$1" + local column="$2" + local conditions="${3:-1=1}" + + db_query "SELECT AVG($column) FROM $table WHERE $conditions;" "-list" +} + +db_min() { + local table="$1" + local column="$2" + local conditions="${3:-1=1}" + + db_query "SELECT MIN($column) FROM $table WHERE $conditions;" "-list" +} + +db_max() { + local table="$1" + local column="$2" + local conditions="${3:-1=1}" + + db_query "SELECT MAX($column) FROM $table WHERE $conditions;" "-list" +} + +#─────────────────────────────────────────────────────────────────────────────── +# BACKUP & RESTORE +#─────────────────────────────────────────────────────────────────────────────── + +db_backup() { + local backup_file="${1:-$DB_DIR/backup_$(date +%Y%m%d_%H%M%S).db}" + + cp "$CURRENT_DB" "$backup_file" + printf "${BR_GREEN}Database backed up to: %s${RST}\n" "$backup_file" +} + +db_restore() { + local backup_file="$1" + + if [[ ! -f "$backup_file" ]]; then + printf "${BR_RED}Backup file not found: %s${RST}\n" "$backup_file" + return 1 + fi + + cp "$backup_file" "$CURRENT_DB" + printf "${BR_GREEN}Database restored from: %s${RST}\n" "$backup_file" +} + +db_export_sql() { + local output_file="${1:-$DB_DIR/export_$(date +%Y%m%d_%H%M%S).sql}" + + sqlite3 "$CURRENT_DB" ".dump" > "$output_file" + printf "${BR_GREEN}Database exported to: %s${RST}\n" "$output_file" +} + +db_import_sql() { + local sql_file="$1" + + if [[ ! -f "$sql_file" ]]; then + printf "${BR_RED}SQL file not found: %s${RST}\n" "$sql_file" + return 1 + fi + + sqlite3 "$CURRENT_DB" < "$sql_file" + printf "${BR_GREEN}Database imported from: %s${RST}\n" "$sql_file" +} + +#─────────────────────────────────────────────────────────────────────────────── +# INTERACTIVE SHELL +#─────────────────────────────────────────────────────────────────────────────── + +db_shell() { + clear_screen + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 💾 BLACKROAD DATABASE SHELL ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + printf "Database: ${BR_YELLOW}%s${RST}\n\n" "$CURRENT_DB" + + # Show tables + printf "${BOLD}Tables:${RST} " + db_list_tables | tr '\n' ' ' + printf "\n\n" + + printf "${TEXT_SECONDARY}Type SQL queries, or commands: .tables, .schema , .quit${RST}\n\n" + + while true; do + printf "${BR_CYAN}sql>${RST} " + read -r query + + case "$query" in + .quit|.exit|.q) + break + ;; + .tables) + db_list_tables + ;; + .schema*) + local table="${query#.schema }" + db_table_info "$table" + ;; + .backup) + db_backup + ;; + .help) + printf "${TEXT_SECONDARY}" + printf ".tables - List tables\n" + printf ".schema - Show table schema\n" + printf ".backup - Backup database\n" + printf ".quit - Exit shell\n" + printf "${RST}" + ;; + "") + continue + ;; + *) + db_query "$query" + ;; + esac + printf "\n" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# METRICS DASHBOARD STORAGE +#─────────────────────────────────────────────────────────────────────────────── + +# Initialize metrics tables +init_metrics_tables() { + db_connect + + db_create_table "metrics" \ + "name TEXT NOT NULL" \ + "value REAL NOT NULL" \ + "unit TEXT" \ + "tags TEXT" + + db_create_table "events" \ + "type TEXT NOT NULL" \ + "message TEXT NOT NULL" \ + "level TEXT DEFAULT 'info'" \ + "source TEXT" + + db_create_table "alerts" \ + "name TEXT NOT NULL" \ + "condition TEXT NOT NULL" \ + "threshold REAL" \ + "status TEXT DEFAULT 'active'" \ + "last_triggered DATETIME" + + printf "${BR_GREEN}Metrics tables initialized${RST}\n" +} + +# Store metric +store_metric() { + local name="$1" + local value="$2" + local unit="${3:-}" + local tags="${4:-}" + + orm_create "metrics" "name=$name" "value=$value" "unit=$unit" "tags=$tags" +} + +# Get metric history +get_metric_history() { + local name="$1" + local limit="${2:-100}" + + qb_reset + qb_table "metrics" + qb_select "value, created_at" + qb_where "name = '$name'" + qb_order "created_at DESC" + qb_limit "$limit" + qb_get +} + +# Store event +store_event() { + local type="$1" + local message="$2" + local level="${3:-info}" + local source="${4:-system}" + + orm_create "events" "type=$type" "message=$message" "level=$level" "source=$source" +} + +# Get recent events +get_recent_events() { + local limit="${1:-50}" + local level="${2:-}" + + qb_reset + qb_table "events" + qb_order "created_at DESC" + qb_limit "$limit" + [[ -n "$level" ]] && qb_where "level = '$level'" + qb_get +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + check_sqlite || exit 1 + + case "${1:-shell}" in + shell) + db_connect + db_shell + ;; + init) + db_connect "${2:-}" + init_migrations + init_metrics_tables + ;; + tables) + db_connect "${2:-}" + db_list_tables + ;; + query) + db_connect + db_query "$2" + ;; + backup) + db_connect + db_backup "$2" + ;; + restore) + db_connect + db_restore "$2" + ;; + export) + db_connect + db_export_sql "$2" + ;; + import) + db_connect + db_import_sql "$2" + ;; + migrations) + db_connect + init_migrations + list_migrations + ;; + seed) + db_connect + # Example: seed_table users 10 "name:string" "email:email" "age:int" + seed_table "$2" "$3" "${@:4}" + ;; + *) + printf "BlackRoad Database ORM v3.0\n\n" + printf "Usage: %s [command] [options]\n\n" "$0" + printf "Commands:\n" + printf " shell Interactive SQL shell\n" + printf " init [db] Initialize database and tables\n" + printf " tables [db] List all tables\n" + printf " query Execute SQL query\n" + printf " backup [file] Backup database\n" + printf " restore Restore from backup\n" + printf " export [file] Export as SQL\n" + printf " import Import SQL file\n" + printf " migrations List migrations\n" + printf " seed
Seed table with data\n" + ;; + esac +fi diff --git a/docker-manager.sh b/docker-manager.sh new file mode 100644 index 0000000..3a7b0ed --- /dev/null +++ b/docker-manager.sh @@ -0,0 +1,614 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ ███████╗██╗ ███████╗███████╗████████╗ +# ██╔══██╗██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗ ██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝ +# ██║ ██║██║ ██║██║ █████╔╝ █████╗ ██████╔╝ █████╗ ██║ █████╗ █████╗ ██║ +# ██║ ██║██║ ██║██║ ██╔═██╗ ██╔══╝ ██╔══██╗ ██╔══╝ ██║ ██╔══╝ ██╔══╝ ██║ +# ██████╔╝╚██████╔╝╚██████╗██║ ██╗███████╗██║ ██║ ██║ ███████╗███████╗███████╗ ██║ +# ╚═════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD DOCKER FLEET MANAGER v3.0 +# Container Orchestration, Monitoring & Management Dashboard +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# DOCKER DETECTION +#─────────────────────────────────────────────────────────────────────────────── + +check_docker() { + if ! command -v docker &>/dev/null; then + printf "${BR_RED}Docker is not installed!${RST}\n" + return 1 + fi + + if ! docker info &>/dev/null; then + printf "${BR_RED}Docker daemon is not running!${RST}\n" + return 1 + fi + + return 0 +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONTAINER OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +get_containers() { + local filter="${1:-all}" # all, running, stopped + + case "$filter" in + running) docker ps --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' 2>/dev/null ;; + stopped) docker ps -a --filter "status=exited" --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' 2>/dev/null ;; + *) docker ps -a --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}' 2>/dev/null ;; + esac +} + +get_container_count() { + docker ps -q 2>/dev/null | wc -l | tr -d ' ' +} + +get_container_total() { + docker ps -aq 2>/dev/null | wc -l | tr -d ' ' +} + +get_container_stats() { + local container_id="$1" + docker stats --no-stream --format '{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}' "$container_id" 2>/dev/null +} + +get_container_logs() { + local container_id="$1" + local lines="${2:-50}" + docker logs --tail "$lines" "$container_id" 2>&1 +} + +get_container_inspect() { + local container_id="$1" + docker inspect "$container_id" 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# IMAGE OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +get_images() { + docker images --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}' 2>/dev/null +} + +get_image_count() { + docker images -q 2>/dev/null | wc -l | tr -d ' ' +} + +get_dangling_images() { + docker images -f "dangling=true" -q 2>/dev/null | wc -l | tr -d ' ' +} + +#─────────────────────────────────────────────────────────────────────────────── +# VOLUME & NETWORK OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +get_volumes() { + docker volume ls --format '{{.Name}}|{{.Driver}}|{{.Mountpoint}}' 2>/dev/null +} + +get_volume_count() { + docker volume ls -q 2>/dev/null | wc -l | tr -d ' ' +} + +get_networks() { + docker network ls --format '{{.ID}}|{{.Name}}|{{.Driver}}|{{.Scope}}' 2>/dev/null +} + +get_network_count() { + docker network ls -q 2>/dev/null | wc -l | tr -d ' ' +} + +#─────────────────────────────────────────────────────────────────────────────── +# DOCKER COMPOSE +#─────────────────────────────────────────────────────────────────────────────── + +detect_compose_files() { + local search_dir="${1:-.}" + find "$search_dir" -maxdepth 3 \( -name "docker-compose.yml" -o -name "docker-compose.yaml" -o -name "compose.yml" -o -name "compose.yaml" \) 2>/dev/null +} + +get_compose_services() { + local compose_file="$1" + if [[ -f "$compose_file" ]]; then + grep -E "^ [a-zA-Z0-9_-]+:" "$compose_file" 2>/dev/null | sed 's/://g' | awk '{print $1}' + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# RESOURCE USAGE +#─────────────────────────────────────────────────────────────────────────────── + +get_docker_disk_usage() { + docker system df 2>/dev/null +} + +get_docker_disk_usage_json() { + docker system df --format '{{json .}}' 2>/dev/null +} + +calculate_total_resources() { + local total_cpu=0 + local total_mem=0 + + while IFS='|' read -r cpu mem_usage mem_perc net block; do + # Extract numeric CPU percentage + local cpu_num=${cpu%\%} + cpu_num=${cpu_num//[^0-9.]/} + + # Extract numeric memory percentage + local mem_num=${mem_perc%\%} + mem_num=${mem_num//[^0-9.]/} + + total_cpu=$(echo "$total_cpu + ${cpu_num:-0}" | bc 2>/dev/null || echo "$total_cpu") + total_mem=$(echo "$total_mem + ${mem_num:-0}" | bc 2>/dev/null || echo "$total_mem") + done < <(docker stats --no-stream --format '{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null) + + printf "%.1f|%.1f" "$total_cpu" "$total_mem" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONTAINER ACTIONS +#─────────────────────────────────────────────────────────────────────────────── + +container_start() { + local container="$1" + docker start "$container" 2>&1 +} + +container_stop() { + local container="$1" + docker stop "$container" 2>&1 +} + +container_restart() { + local container="$1" + docker restart "$container" 2>&1 +} + +container_remove() { + local container="$1" + local force="${2:-false}" + + if [[ "$force" == "true" ]]; then + docker rm -f "$container" 2>&1 + else + docker rm "$container" 2>&1 + fi +} + +container_exec() { + local container="$1" + local cmd="$2" + docker exec "$container" $cmd 2>&1 +} + +container_shell() { + local container="$1" + local shell="${2:-/bin/sh}" + docker exec -it "$container" "$shell" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CLEANUP OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +cleanup_stopped_containers() { + docker container prune -f 2>&1 +} + +cleanup_dangling_images() { + docker image prune -f 2>&1 +} + +cleanup_unused_volumes() { + docker volume prune -f 2>&1 +} + +cleanup_unused_networks() { + docker network prune -f 2>&1 +} + +cleanup_all() { + docker system prune -af --volumes 2>&1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# VISUALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +render_container_status() { + local status="$1" + + if [[ "$status" == *"Up"* ]]; then + printf "${BR_GREEN}●${RST}" + elif [[ "$status" == *"Exited"* ]]; then + printf "${BR_RED}●${RST}" + elif [[ "$status" == *"Restarting"* ]]; then + printf "${BR_YELLOW}◐${RST}" + elif [[ "$status" == *"Paused"* ]]; then + printf "${BR_BLUE}◑${RST}" + else + printf "${TEXT_MUTED}○${RST}" + fi +} + +render_resource_bar() { + local value="$1" + local max="${2:-100}" + local width="${3:-20}" + local label="${4:-}" + + local filled=$((value * width / max)) + [[ $filled -gt $width ]] && filled=$width + local empty=$((width - filled)) + + local color="$BR_GREEN" + [[ $value -gt 50 ]] && color="$BR_YELLOW" + [[ $value -gt 80 ]] && color="$BR_RED" + + printf "%s" "$color" + printf "%0.s█" $(seq 1 $filled 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s░" $(seq 1 $empty 2>/dev/null) || true + printf "${RST}" + + [[ -n "$label" ]] && printf " %s" "$label" +} + +render_container_card() { + local id="$1" + local name="$2" + local image="$3" + local status="$4" + local ports="$5" + + local stats=$(get_container_stats "$id") + IFS='|' read -r cpu mem_usage mem_perc net block <<< "$stats" + + printf "${BR_CYAN}┌─────────────────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_CYAN}│${RST} " + render_container_status "$status" + printf " ${BOLD}%-20s${RST} ${TEXT_MUTED}%s${RST}%*s${BR_CYAN}│${RST}\n" "${name:0:20}" "${id:0:12}" $((29 - ${#id})) "" + + printf "${BR_CYAN}│${RST} ${TEXT_SECONDARY}Image: %-50s${RST} ${BR_CYAN}│${RST}\n" "${image:0:50}" + printf "${BR_CYAN}│${RST} ${TEXT_SECONDARY}Status: %-49s${RST} ${BR_CYAN}│${RST}\n" "${status:0:49}" + + if [[ -n "$ports" ]]; then + printf "${BR_CYAN}│${RST} ${TEXT_SECONDARY}Ports: %-50s${RST} ${BR_CYAN}│${RST}\n" "${ports:0:50}" + fi + + if [[ -n "$cpu" ]]; then + local cpu_num=${cpu%\%} + cpu_num=${cpu_num//[^0-9]/} + local mem_num=${mem_perc%\%} + mem_num=${mem_num//[^0-9]/} + + printf "${BR_CYAN}│${RST} CPU: " + render_resource_bar "${cpu_num:-0}" 100 15 + printf " %-6s MEM: " "$cpu" + render_resource_bar "${mem_num:-0}" 100 15 + printf " %-6s ${BR_CYAN}│${RST}\n" "$mem_perc" + fi + + printf "${BR_CYAN}└─────────────────────────────────────────────────────────────────┘${RST}\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONTAINER LIST VIEW +#─────────────────────────────────────────────────────────────────────────────── + +render_container_list() { + local filter="${1:-all}" + + printf "${BOLD}%-4s %-15s %-25s %-15s %-20s${RST}\n" "ST" "NAME" "IMAGE" "STATUS" "PORTS" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + + local count=0 + while IFS='|' read -r id name image status ports; do + [[ -z "$id" ]] && continue + + printf " " + render_container_status "$status" + printf " ${BR_CYAN}%-15s${RST} " "${name:0:15}" + printf "${TEXT_SECONDARY}%-25s${RST} " "${image:0:25}" + + if [[ "$status" == *"Up"* ]]; then + printf "${BR_GREEN}%-15s${RST} " "${status:0:15}" + else + printf "${BR_RED}%-15s${RST} " "${status:0:15}" + fi + + printf "${TEXT_MUTED}%-20s${RST}\n" "${ports:0:20}" + + ((count++)) + [[ $count -ge 15 ]] && break + done < <(get_containers "$filter") + + local total=$(get_container_total) + [[ $count -lt $total ]] && printf "${TEXT_MUTED}... and %d more containers${RST}\n" "$((total - count))" +} + +#─────────────────────────────────────────────────────────────────────────────── +# IMAGE LIST VIEW +#─────────────────────────────────────────────────────────────────────────────── + +render_image_list() { + printf "${BOLD}%-15s %-30s %-10s %-12s${RST}\n" "ID" "REPOSITORY" "TAG" "SIZE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + + local count=0 + while IFS='|' read -r id repo tag size created; do + [[ -z "$id" ]] && continue + + printf " ${BR_YELLOW}%-13s${RST} " "${id:0:12}" + printf "${BR_CYAN}%-30s${RST} " "${repo:0:30}" + printf "${TEXT_SECONDARY}%-10s${RST} " "${tag:0:10}" + printf "${BR_PURPLE}%-12s${RST}\n" "${size:0:12}" + + ((count++)) + [[ $count -ge 10 ]] && break + done < <(get_images) +} + +#─────────────────────────────────────────────────────────────────────────────── +# STATS DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_live_stats() { + printf "${BOLD}Live Container Statistics${RST}\n\n" + + printf "%-20s %-10s %-20s %-20s %-15s\n" "CONTAINER" "CPU" "MEMORY" "NET I/O" "BLOCK I/O" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────────" + + docker stats --no-stream --format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null | \ + while IFS='|' read -r name cpu mem net block; do + printf "${BR_CYAN}%-20s${RST} " "${name:0:20}" + printf "${BR_GREEN}%-10s${RST} " "$cpu" + printf "${BR_YELLOW}%-20s${RST} " "$mem" + printf "${TEXT_SECONDARY}%-20s${RST} " "$net" + printf "${TEXT_MUTED}%-15s${RST}\n" "$block" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_docker_dashboard() { + clear_screen + cursor_hide + + if ! check_docker; then + sleep 2 + return 1 + fi + + # Header + printf "${BR_BLUE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🐳 DOCKER FLEET MANAGER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Summary stats + local running=$(get_container_count) + local total=$(get_container_total) + local images=$(get_image_count) + local volumes=$(get_volume_count) + local networks=$(get_network_count) + local dangling=$(get_dangling_images) + + printf " ${BOLD}Running:${RST} ${BR_GREEN}%s${RST}/${TEXT_MUTED}%s${RST} " "$running" "$total" + printf "${BOLD}Images:${RST} ${BR_PURPLE}%s${RST} " "$images" + printf "${BOLD}Volumes:${RST} ${BR_YELLOW}%s${RST} " "$volumes" + printf "${BOLD}Networks:${RST} ${BR_CYAN}%s${RST} " "$networks" + [[ $dangling -gt 0 ]] && printf "${BR_RED}(%d dangling)${RST}" "$dangling" + printf "\n\n" + + # Two-column layout + printf "${BR_CYAN}┌───────────────────────────────────────┬──────────────────────────────────────┐${RST}\n" + printf "${BR_CYAN}│${RST} ${BOLD}Running Containers${RST} ${BR_CYAN}│${RST} ${BOLD}Resource Usage${RST} ${BR_CYAN}│${RST}\n" + printf "${BR_CYAN}├───────────────────────────────────────┼──────────────────────────────────────┤${RST}\n" + + # Left column - running containers + local left_lines=() + while IFS='|' read -r id name image status ports; do + [[ -z "$id" ]] && continue + local line=$(printf "%-12s %-20s" "${name:0:12}" "${status:0:20}") + left_lines+=("$line") + done < <(get_containers "running" | head -8) + + # Right column - stats + local right_lines=() + while IFS='|' read -r name cpu mem net block; do + [[ -z "$name" ]] && continue + local cpu_num=${cpu%\%} + local line=$(printf "%-12s CPU:%-6s MEM:%-8s" "${name:0:12}" "$cpu" "${mem%%/*}") + right_lines+=("$line") + done < <(docker stats --no-stream --format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null | head -8) + + # Render rows + local max_rows=8 + for ((i=0; i/dev/null; then + case "$key" in + s|S) + printf "\n${BR_CYAN}Container name to start: ${RST}" + cursor_show + read -r name + cursor_hide + [[ -n "$name" ]] && container_start "$name" + sleep 1 + ;; + t|T) + printf "\n${BR_CYAN}Container name to stop: ${RST}" + cursor_show + read -r name + cursor_hide + [[ -n "$name" ]] && container_stop "$name" + sleep 1 + ;; + r|R) + printf "\n${BR_CYAN}Container name to restart: ${RST}" + cursor_show + read -r name + cursor_hide + [[ -n "$name" ]] && container_restart "$name" + sleep 1 + ;; + k|K) + printf "\n${BR_CYAN}Container name to remove: ${RST}" + cursor_show + read -r name + cursor_hide + [[ -n "$name" ]] && container_remove "$name" "true" + sleep 1 + ;; + l|L) + printf "\n${BR_CYAN}Container name for logs: ${RST}" + cursor_show + read -r name + cursor_hide + if [[ -n "$name" ]]; then + clear_screen + printf "${BOLD}Logs for %s:${RST}\n\n" "$name" + get_container_logs "$name" 30 + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + fi + ;; + e|E) + printf "\n${BR_CYAN}Container name: ${RST}" + cursor_show + read -r name + printf "${BR_CYAN}Command: ${RST}" + read -r cmd + cursor_hide + if [[ -n "$name" && -n "$cmd" ]]; then + container_exec "$name" "$cmd" + sleep 2 + fi + ;; + i|I) + clear_screen + printf "${BOLD}Docker Images${RST}\n\n" + render_image_list + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + c|C) + printf "\n${BR_YELLOW}Cleanup Menu:${RST}\n" + printf " 1. Stopped containers\n" + printf " 2. Dangling images\n" + printf " 3. Unused volumes\n" + printf " 4. Unused networks\n" + printf " 5. Everything\n" + read -rsn1 choice + case "$choice" in + 1) cleanup_stopped_containers ;; + 2) cleanup_dangling_images ;; + 3) cleanup_unused_volumes ;; + 4) cleanup_unused_networks ;; + 5) cleanup_all ;; + esac + sleep 2 + ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# DOCKER COMPOSE MANAGER +#─────────────────────────────────────────────────────────────────────────────── + +compose_manager() { + local compose_file="${1:-docker-compose.yml}" + + clear_screen + printf "${BOLD}Docker Compose Manager${RST}\n\n" + + if [[ ! -f "$compose_file" ]]; then + printf "${BR_RED}Compose file not found: %s${RST}\n" "$compose_file" + return 1 + fi + + printf "${BR_CYAN}File: %s${RST}\n\n" "$compose_file" + + printf "${BOLD}Services:${RST}\n" + get_compose_services "$compose_file" | while read -r service; do + printf " ${BR_GREEN}●${RST} %s\n" "$service" + done + + printf "\n${TEXT_SECONDARY}[u]p [d]own [r]estart [l]ogs [p]s [b]uild [q]uit${RST}\n" + + while true; do + read -rsn1 key + case "$key" in + u) docker-compose -f "$compose_file" up -d ;; + d) docker-compose -f "$compose_file" down ;; + r) docker-compose -f "$compose_file" restart ;; + l) docker-compose -f "$compose_file" logs --tail=50 ;; + p) docker-compose -f "$compose_file" ps ;; + b) docker-compose -f "$compose_file" build ;; + q) break ;; + esac + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) docker_dashboard_loop ;; + list) render_container_list "${2:-all}" ;; + stats) render_live_stats ;; + images) render_image_list ;; + start) container_start "$2" ;; + stop) container_stop "$2" ;; + restart) container_restart "$2" ;; + remove) container_remove "$2" "${3:-false}" ;; + logs) get_container_logs "$2" "${3:-50}" ;; + exec) container_exec "$2" "$3" ;; + shell) container_shell "$2" "${3:-/bin/sh}" ;; + clean) cleanup_all ;; + compose) compose_manager "$2" ;; + *) + printf "Usage: %s [dashboard|list|stats|images|start|stop|restart|remove|logs|exec|shell|clean|compose]\n" "$0" + ;; + esac +fi diff --git a/file-manager.sh b/file-manager.sh new file mode 100644 index 0000000..1f9e765 --- /dev/null +++ b/file-manager.sh @@ -0,0 +1,453 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███████╗██╗██╗ ███████╗███████╗ +# ██╔════╝██║██║ ██╔════╝██╔════╝ +# █████╗ ██║██║ █████╗ ███████╗ +# ██╔══╝ ██║██║ ██╔══╝ ╚════██║ +# ██║ ██║███████╗███████╗███████║ +# ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD FILE MANAGER v3.0 +# Terminal File Browser with Preview & Operations +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +CURRENT_DIR="$(pwd)" +SELECTED_IDX=0 +SCROLL_OFFSET=0 +MAX_DISPLAY=20 +SHOW_HIDDEN=false +SORT_BY="name" +CLIPBOARD="" +CLIPBOARD_OP="" +PREVIEW_WIDTH=40 + +#─────────────────────────────────────────────────────────────────────────────── +# FILE LISTING +#─────────────────────────────────────────────────────────────────────────────── + +get_files() { + local dir="$1" + local hidden_flag="" + [[ "$SHOW_HIDDEN" == "true" ]] && hidden_flag="-A" + + local sort_flag="-v" + case "$SORT_BY" in + size) sort_flag="-S" ;; + time) sort_flag="-t" ;; + type) sort_flag="-X" ;; + esac + + ls -1 $hidden_flag $sort_flag "$dir" 2>/dev/null +} + +get_file_info() { + local file="$1" + + if [[ -d "$file" ]]; then + echo "dir|$(ls -1 "$file" 2>/dev/null | wc -l) items" + elif [[ -L "$file" ]]; then + echo "link|$(readlink "$file")" + elif [[ -x "$file" ]]; then + echo "exec|$(stat -c %s "$file" 2>/dev/null || stat -f %z "$file" 2>/dev/null)" + else + local size=$(stat -c %s "$file" 2>/dev/null || stat -f %z "$file" 2>/dev/null || echo "0") + local ext="${file##*.}" + echo "file|$size|$ext" + fi +} + +format_size() { + local bytes="$1" + + if [[ $bytes -ge 1073741824 ]]; then + printf "%.1fG" "$(echo "$bytes / 1073741824" | bc -l)" + elif [[ $bytes -ge 1048576 ]]; then + printf "%.1fM" "$(echo "$bytes / 1048576" | bc -l)" + elif [[ $bytes -ge 1024 ]]; then + printf "%.1fK" "$(echo "$bytes / 1024" | bc -l)" + else + printf "%dB" "$bytes" + fi +} + +get_file_icon() { + local file="$1" + local ext="${file##*.}" + + if [[ -d "$file" ]]; then + echo "📁" + elif [[ -L "$file" ]]; then + echo "🔗" + elif [[ -x "$file" ]]; then + echo "⚡" + else + case "${ext,,}" in + sh|bash|zsh) echo "📜" ;; + py) echo "🐍" ;; + js|ts) echo "📦" ;; + json) echo "📋" ;; + md|txt) echo "📄" ;; + jpg|jpeg|png|gif) echo "🖼️" ;; + mp3|wav|flac) echo "🎵" ;; + mp4|mkv|avi) echo "🎬" ;; + zip|tar|gz) echo "📦" ;; + pdf) echo "📕" ;; + html|css) echo "🌐" ;; + *) echo "📄" ;; + esac + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# PREVIEW +#─────────────────────────────────────────────────────────────────────────────── + +preview_file() { + local file="$1" + local height="${2:-15}" + local width="${3:-40}" + + if [[ -d "$file" ]]; then + printf "\033[1mDirectory Contents:\033[0m\n" + ls -la "$file" 2>/dev/null | head -n "$height" + elif [[ -f "$file" ]]; then + local mime=$(file -b --mime-type "$file" 2>/dev/null) + + case "$mime" in + text/*|application/json|application/javascript) + head -n "$height" "$file" 2>/dev/null | cut -c1-"$width" + ;; + image/*) + printf "\033[38;5;240mImage: %s\033[0m\n" "$(file -b "$file")" + ;; + *) + printf "\033[38;5;240mBinary: %s\033[0m\n" "$mime" + file -b "$file" + ;; + esac + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# FILE OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +copy_file() { + CLIPBOARD="$1" + CLIPBOARD_OP="copy" +} + +cut_file() { + CLIPBOARD="$1" + CLIPBOARD_OP="cut" +} + +paste_file() { + local dest="$1" + + [[ -z "$CLIPBOARD" ]] && return + + if [[ "$CLIPBOARD_OP" == "copy" ]]; then + cp -r "$CLIPBOARD" "$dest/" + elif [[ "$CLIPBOARD_OP" == "cut" ]]; then + mv "$CLIPBOARD" "$dest/" + CLIPBOARD="" + CLIPBOARD_OP="" + fi +} + +delete_file() { + local file="$1" + rm -rf "$file" +} + +rename_file() { + local old="$1" + local new="$2" + mv "$old" "$new" +} + +create_dir() { + local name="$1" + mkdir -p "$name" +} + +create_file() { + local name="$1" + touch "$name" +} + +#─────────────────────────────────────────────────────────────────────────────── +# RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +render_header() { + printf "\033[1;38;5;214m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 📂 BLACKROAD FILE MANAGER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m" + + printf "\n \033[38;5;51m%s\033[0m\n" "$CURRENT_DIR" + printf " \033[38;5;240mSort: %s Hidden: %s\033[0m\n\n" "$SORT_BY" "$SHOW_HIDDEN" +} + +render_file_list() { + local -a files + mapfile -t files < <(get_files "$CURRENT_DIR") + + local total=${#files[@]} + + # Add parent directory + [[ "$CURRENT_DIR" != "/" ]] && files=(".." "${files[@]}") + + total=${#files[@]} + + # Adjust selection + [[ $SELECTED_IDX -lt 0 ]] && SELECTED_IDX=0 + [[ $SELECTED_IDX -ge $total ]] && SELECTED_IDX=$((total - 1)) + [[ $SELECTED_IDX -lt $SCROLL_OFFSET ]] && SCROLL_OFFSET=$SELECTED_IDX + [[ $SELECTED_IDX -ge $((SCROLL_OFFSET + MAX_DISPLAY)) ]] && SCROLL_OFFSET=$((SELECTED_IDX - MAX_DISPLAY + 1)) + + local list_width=$((80 - PREVIEW_WIDTH - 5)) + + # Two-column layout + printf "\033[38;5;240m┌" + printf "%0.s─" $(seq 1 $list_width) + printf "┬" + printf "%0.s─" $(seq 1 $PREVIEW_WIDTH) + printf "┐\033[0m\n" + + local selected_file="" + + for ((i=0; i/dev/null; then + less "$full_path" + else + cat "$full_path" | head -50 + read -rsn1 + fi + tput civis + clear + fi + ;; + h|H) + [[ "$SHOW_HIDDEN" == "true" ]] && SHOW_HIDDEN=false || SHOW_HIDDEN=true + SELECTED_IDX=0 + SCROLL_OFFSET=0 + ;; + s|S) + case "$SORT_BY" in + name) SORT_BY="size" ;; + size) SORT_BY="time" ;; + time) SORT_BY="type" ;; + *) SORT_BY="name" ;; + esac + SELECTED_IDX=0 + ;; + c|C) + copy_file "$full_path" + ;; + x|X) + cut_file "$full_path" + ;; + v|V) + paste_file "$CURRENT_DIR" + ;; + d|D) + tput cnorm + printf "\n Delete '%s'? (y/n): " "$selected" + read -rsn1 confirm + [[ "$confirm" == "y" ]] && delete_file "$full_path" + tput civis + clear + ;; + r|R) + tput cnorm + printf "\n New name: " + read -r new_name + [[ -n "$new_name" ]] && rename_file "$full_path" "$CURRENT_DIR/$new_name" + tput civis + clear + ;; + n|N) + tput cnorm + printf "\n Create (f)ile or (d)irectory? " + read -rsn1 type + printf "\n Name: " + read -r name + if [[ "$type" == "d" ]]; then + create_dir "$CURRENT_DIR/$name" + else + create_file "$CURRENT_DIR/$name" + fi + tput civis + clear + ;; + /) + tput cnorm + printf "\n Search: " + read -r pattern + # Jump to match + for ((i=0; i<${#files[@]}; i++)); do + if [[ "${files[$i]}" == *"$pattern"* ]]; then + SELECTED_IDX=$i + break + fi + done + tput civis + ;; + q|Q) + tput cnorm + clear + exit 0 + ;; + esac + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + CURRENT_DIR="${1:-$(pwd)}" + file_manager_loop +fi diff --git a/git-dashboard.sh b/git-dashboard.sh new file mode 100644 index 0000000..10d656d --- /dev/null +++ b/git-dashboard.sh @@ -0,0 +1,393 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██╗████████╗ ██████╗ █████╗ ███████╗██╗ ██╗██████╗ ██████╗ █████╗ ██████╗ ██████╗ +# ██╔════╝ ██║╚══██╔══╝ ██╔══██╗██╔══██╗██╔════╝██║ ██║██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██╔══██╗ +# ██║ ███╗██║ ██║ ██║ ██║███████║███████╗███████║██████╔╝██║ ██║███████║██████╔╝██║ ██║ +# ██║ ██║██║ ██║ ██║ ██║██╔══██║╚════██║██╔══██║██╔══██╗██║ ██║██╔══██║██╔══██╗██║ ██║ +# ╚██████╔╝██║ ██║ ██████╔╝██║ ██║███████║██║ ██║██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝ +# ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD GIT OPERATIONS DASHBOARD v3.0 +# Visual Git Management, History, Branches, Commits +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# GIT DETECTION +#─────────────────────────────────────────────────────────────────────────────── + +is_git_repo() { + git rev-parse --is-inside-work-tree &>/dev/null +} + +get_repo_root() { + git rev-parse --show-toplevel 2>/dev/null +} + +get_current_branch() { + git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null +} + +get_remote_url() { + git remote get-url origin 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# GIT STATISTICS +#─────────────────────────────────────────────────────────────────────────────── + +get_commit_count() { + git rev-list --count HEAD 2>/dev/null || echo "0" +} + +get_branch_count() { + git branch -a 2>/dev/null | wc -l | tr -d ' ' +} + +get_contributor_count() { + git shortlog -sn 2>/dev/null | wc -l | tr -d ' ' +} + +get_file_count() { + git ls-files 2>/dev/null | wc -l | tr -d ' ' +} + +get_repo_size() { + local size=$(du -sh "$(get_repo_root)/.git" 2>/dev/null | awk '{print $1}') + echo "${size:-0}" +} + +get_last_commit() { + git log -1 --format="%h|%s|%ar|%an" 2>/dev/null +} + +get_uncommitted_changes() { + git status --porcelain 2>/dev/null | wc -l | tr -d ' ' +} + +#─────────────────────────────────────────────────────────────────────────────── +# GIT VISUALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +# Commit activity graph (last 7 days) +render_commit_graph() { + local days=7 + local -a counts=() + local max_count=1 + + for ((i=days-1; i>=0; i--)); do + local date_str=$(date -d "$i days ago" +%Y-%m-%d 2>/dev/null || date -v-${i}d +%Y-%m-%d 2>/dev/null) + local count=$(git log --oneline --since="$date_str 00:00" --until="$date_str 23:59" 2>/dev/null | wc -l | tr -d ' ') + counts+=("$count") + [[ $count -gt $max_count ]] && max_count=$count + done + + printf "${BOLD}Commit Activity (7 days)${RST}\n" + + # Bar chart + for ((row=5; row>=1; row--)); do + printf " " + for count in "${counts[@]}"; do + local height=$((count * 5 / max_count)) + if [[ $height -ge $row ]]; then + printf "${BR_GREEN}██${RST} " + else + printf " " + fi + done + printf "\n" + done + + # Labels + printf " " + for ((i=days-1; i>=0; i--)); do + local day=$(date -d "$i days ago" +%a 2>/dev/null || date -v-${i}d +%a 2>/dev/null) + printf "${TEXT_MUTED}%-3s${RST}" "${day:0:2}" + done + printf "\n" +} + +# Branch visualization +render_branch_tree() { + printf "${BOLD}Branches${RST}\n\n" + + local current=$(get_current_branch) + + # Local branches + while IFS= read -r branch; do + branch="${branch# }" + branch="${branch#\* }" + + if [[ "$branch" == "$current" ]]; then + printf " ${BR_GREEN}●${RST} ${BOLD}${BR_CYAN}%s${RST} ${BR_GREEN}(current)${RST}\n" "$branch" + else + printf " ${TEXT_MUTED}○${RST} ${TEXT_SECONDARY}%s${RST}\n" "$branch" + fi + done < <(git branch 2>/dev/null | head -10) + + local branch_count=$(git branch 2>/dev/null | wc -l) + [[ $branch_count -gt 10 ]] && printf " ${TEXT_MUTED}... and %d more${RST}\n" "$((branch_count - 10))" +} + +# Recent commits list +render_recent_commits() { + printf "${BOLD}Recent Commits${RST}\n\n" + + local format="%C(yellow)%h%C(reset) %C(cyan)%s%C(reset) %C(dim)(%ar by %an)%C(reset)" + + git log --oneline --decorate -10 2>/dev/null | while IFS= read -r line; do + local hash=$(echo "$line" | awk '{print $1}') + local msg=$(echo "$line" | cut -d' ' -f2-) + + printf " ${BR_YELLOW}%s${RST} ${TEXT_SECONDARY}%s${RST}\n" "$hash" "${msg:0:50}" + done +} + +# File changes summary +render_changes_summary() { + printf "${BOLD}Working Tree${RST}\n\n" + + local staged=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ') + local unstaged=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ') + local untracked=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ') + + printf " ${BR_GREEN}●${RST} Staged: %d files\n" "$staged" + printf " ${BR_YELLOW}●${RST} Modified: %d files\n" "$unstaged" + printf " ${BR_RED}●${RST} Untracked: %d files\n" "$untracked" + + # Show actual changes + if [[ $((staged + unstaged + untracked)) -gt 0 ]]; then + printf "\n" + git status --porcelain 2>/dev/null | head -8 | while IFS= read -r line; do + local status="${line:0:2}" + local file="${line:3}" + + case "$status" in + "A "*|"M "*) printf " ${BR_GREEN}+${RST} %s\n" "$file" ;; + " M"|"MM") printf " ${BR_YELLOW}~${RST} %s\n" "$file" ;; + "??") printf " ${BR_RED}?${RST} %s\n" "$file" ;; + "D "*|" D") printf " ${BR_RED}-${RST} %s\n" "$file" ;; + *) printf " ${TEXT_MUTED}%s${RST} %s\n" "$status" "$file" ;; + esac + done + fi +} + +# Contributor stats +render_contributors() { + printf "${BOLD}Top Contributors${RST}\n\n" + + git shortlog -sn --no-merges 2>/dev/null | head -5 | while IFS=$'\t' read -r count name; do + local bar_len=$((count / 10)) + [[ $bar_len -gt 20 ]] && bar_len=20 + + printf " ${BR_CYAN}%-20s${RST} " "${name:0:20}" + printf "${BR_PURPLE}" + printf "%0.s█" $(seq 1 "$bar_len" 2>/dev/null) || true + printf "${RST} ${TEXT_MUTED}%d${RST}\n" "$count" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# GIT OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +git_quick_commit() { + local message="$1" + + if [[ -z "$message" ]]; then + printf "${BR_CYAN}Commit message: ${RST}" + read -r message + fi + + [[ -z "$message" ]] && message="Quick commit $(date '+%Y-%m-%d %H:%M')" + + git add -A + git commit -m "$message" +} + +git_sync() { + printf "${BR_CYAN}Syncing with remote...${RST}\n" + git pull --rebase && git push +} + +git_stash_menu() { + printf "\n${BOLD}Stash Menu${RST}\n" + printf " ${BR_YELLOW}1.${RST} Stash changes\n" + printf " ${BR_YELLOW}2.${RST} Pop stash\n" + printf " ${BR_YELLOW}3.${RST} List stashes\n" + printf " ${BR_YELLOW}4.${RST} Clear stashes\n" + + read -rsn1 choice + case "$choice" in + 1) git stash push -m "Stash $(date '+%Y-%m-%d %H:%M')" ;; + 2) git stash pop ;; + 3) git stash list ;; + 4) git stash clear ;; + esac +} + +git_branch_menu() { + printf "\n${BOLD}Branch Menu${RST}\n" + + local branches=($(git branch --format='%(refname:short)' 2>/dev/null)) + local idx=1 + + for branch in "${branches[@]}"; do + printf " ${BR_YELLOW}%d.${RST} %s\n" "$idx" "$branch" + ((idx++)) + done + + printf "\n ${BR_GREEN}n.${RST} New branch\n" + + read -rsn1 choice + if [[ "$choice" == "n" ]]; then + printf "${BR_CYAN}New branch name: ${RST}" + read -r new_branch + git checkout -b "$new_branch" + elif [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -le ${#branches[@]} ]]; then + git checkout "${branches[$((choice-1))]}" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_git_dashboard() { + clear_screen + cursor_hide + + if ! is_git_repo; then + printf "${BR_RED}Not a git repository!${RST}\n" + printf "${TEXT_SECONDARY}Navigate to a git repo and try again.${RST}\n" + sleep 2 + return 1 + fi + + # Header + printf "${BR_ORANGE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🔀 GIT OPERATIONS DASHBOARD ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Repository info + local repo_name=$(basename "$(get_repo_root)") + local branch=$(get_current_branch) + local remote=$(get_remote_url) + local commits=$(get_commit_count) + local changes=$(get_uncommitted_changes) + + printf " ${BOLD}Repository:${RST} ${BR_CYAN}%s${RST}\n" "$repo_name" + printf " ${BOLD}Branch:${RST} ${BR_GREEN}%s${RST}\n" "$branch" + printf " ${BOLD}Remote:${RST} ${TEXT_MUTED}%s${RST}\n" "${remote:-none}" + printf " ${BOLD}Commits:${RST} ${TEXT_SECONDARY}%s${RST} ${BOLD}Changes:${RST} " "$commits" + if [[ $changes -gt 0 ]]; then + printf "${BR_YELLOW}%d uncommitted${RST}\n" "$changes" + else + printf "${BR_GREEN}clean${RST}\n" + fi + printf "\n" + + # Two-column layout + printf "${BR_CYAN}┌─────────────────────────────────┬──────────────────────────────────────────┐${RST}\n" + + # Left: Commit graph and branches + printf "${BR_CYAN}│${RST} " + render_commit_graph | head -1 + printf "%35s" "" + printf "${BR_CYAN}│${RST} " + render_branch_tree | head -1 + printf "\n" + + # Content rows (simplified for display) + for ((i=0; i<8; i++)); do + printf "${BR_CYAN}│${RST} %-33s ${BR_CYAN}│${RST} %-40s ${BR_CYAN}│${RST}\n" "" "" + done + + printf "${BR_CYAN}├─────────────────────────────────┴──────────────────────────────────────────┤${RST}\n" + + # Recent commits + printf "${BR_CYAN}│${RST} ${BOLD}Recent Commits${RST}%61s${BR_CYAN}│${RST}\n" "" + + git log --oneline -5 2>/dev/null | while IFS= read -r line; do + local hash=$(echo "$line" | awk '{print $1}') + local msg=$(echo "$line" | cut -d' ' -f2- | head -c 60) + printf "${BR_CYAN}│${RST} ${BR_YELLOW}%s${RST} %-64s ${BR_CYAN}│${RST}\n" "$hash" "$msg" + done + + printf "${BR_CYAN}└──────────────────────────────────────────────────────────────────────────────┘${RST}\n" + + # Quick actions + printf "\n${TEXT_SECONDARY}[c]ommit [p]ush [l]pull [b]ranch [s]tash [d]iff [r]efresh [q]uit${RST}\n" +} + +git_dashboard_loop() { + while true; do + render_git_dashboard + + if read -rsn1 -t 3 key 2>/dev/null; then + case "$key" in + c|C) + cursor_show + printf "\n${BR_CYAN}Commit message: ${RST}" + read -r msg + cursor_hide + git_quick_commit "$msg" + sleep 1 + ;; + p|P) + printf "\n${BR_CYAN}Pushing...${RST}\n" + git push 2>&1 | head -5 + sleep 2 + ;; + l|L) + printf "\n${BR_CYAN}Pulling...${RST}\n" + git pull 2>&1 | head -5 + sleep 2 + ;; + b|B) + cursor_show + git_branch_menu + cursor_hide + sleep 1 + ;; + s|S) + cursor_show + git_stash_menu + cursor_hide + sleep 1 + ;; + d|D) + git diff --stat 2>/dev/null | head -20 + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + r|R) continue ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) git_dashboard_loop ;; + status) git status ;; + log) git log --oneline -20 ;; + commit) git_quick_commit "$2" ;; + sync) git_sync ;; + *) + printf "Usage: %s [dashboard|status|log|commit|sync]\n" "$0" + ;; + esac +fi diff --git a/health-check.sh b/health-check.sh new file mode 100644 index 0000000..2aa56e7 --- /dev/null +++ b/health-check.sh @@ -0,0 +1,574 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██╗███████╗ █████╗ ██╗ ████████╗██╗ ██╗ +# ██║ ██║██╔════╝██╔══██╗██║ ╚══██╔══╝██║ ██║ +# ███████║█████╗ ███████║██║ ██║ ███████║ +# ██╔══██║██╔══╝ ██╔══██║██║ ██║ ██╔══██║ +# ██║ ██║███████╗██║ ██║███████╗██║ ██║ ██║ +# ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD HEALTH CHECK SYSTEM v2.0 +# Unified health monitoring with retry logic and alerting +#═══════════════════════════════════════════════════════════════════════════════ + +# Source libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" +[[ -f "$SCRIPT_DIR/notification-system.sh" ]] && source "$SCRIPT_DIR/notification-system.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +HEALTH_HOME="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards}/health" +HEALTH_LOG="$HEALTH_HOME/health.log" +HEALTH_STATE="$HEALTH_HOME/state.json" +mkdir -p "$HEALTH_HOME" 2>/dev/null + +# Retry configuration +MAX_RETRIES=3 +RETRY_DELAY=2 +TIMEOUT=10 + +# Check intervals (seconds) +CHECK_INTERVAL_FAST=30 +CHECK_INTERVAL_NORMAL=60 +CHECK_INTERVAL_SLOW=300 + +# State tracking +declare -A HEALTH_STATUS +declare -A LAST_CHECK_TIME +declare -A FAILURE_COUNT +declare -A LAST_ALERT_TIME + +#─────────────────────────────────────────────────────────────────────────────── +# RETRY LOGIC +#─────────────────────────────────────────────────────────────────────────────── + +# Execute with retry and exponential backoff +retry_exec() { + local max_attempts="${1:-$MAX_RETRIES}" + local base_delay="${2:-$RETRY_DELAY}" + shift 2 + local cmd=("$@") + + local attempt=1 + local delay="$base_delay" + + while [[ $attempt -le $max_attempts ]]; do + log_debug "Attempt $attempt/$max_attempts: ${cmd[*]}" + + local start_time=$(date +%s%N) + local output + local exit_code + + if output=$("${cmd[@]}" 2>&1); then + local end_time=$(date +%s%N) + local duration=$(( (end_time - start_time) / 1000000 )) + + echo "$output" + return 0 + else + exit_code=$? + fi + + if [[ $attempt -lt $max_attempts ]]; then + log_warn "Attempt $attempt failed (exit=$exit_code), retrying in ${delay}s..." + sleep "$delay" + delay=$((delay * 2)) # Exponential backoff + fi + + ((attempt++)) + done + + log_error "All $max_attempts attempts failed" + return 1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# HEALTH CHECK TYPES +#─────────────────────────────────────────────────────────────────────────────── + +# Ping check with retry +check_ping() { + local host="$1" + local timeout="${2:-$TIMEOUT}" + + retry_exec $MAX_RETRIES $RETRY_DELAY \ + ping -c 1 -W "$timeout" "$host" +} + +# HTTP check with retry +check_http() { + local url="$1" + local expected_code="${2:-200}" + local timeout="${3:-$TIMEOUT}" + + local response + response=$(retry_exec $MAX_RETRIES $RETRY_DELAY \ + curl -s -o /dev/null -w "%{http_code}|%{time_total}|%{size_download}" \ + --connect-timeout "$timeout" --max-time "$((timeout * 2))" "$url") + + if [[ $? -eq 0 ]]; then + local code=$(echo "$response" | cut -d'|' -f1) + local time=$(echo "$response" | cut -d'|' -f2) + local size=$(echo "$response" | cut -d'|' -f3) + + if [[ "$code" == "$expected_code" ]]; then + echo "OK|$code|$time|$size" + return 0 + else + echo "FAIL|$code|$time|$size" + return 1 + fi + fi + + echo "ERROR|0|0|0" + return 1 +} + +# TCP port check +check_port() { + local host="$1" + local port="$2" + local timeout="${3:-$TIMEOUT}" + + retry_exec $MAX_RETRIES $RETRY_DELAY \ + bash -c "timeout $timeout bash -c 'cat < /dev/null > /dev/tcp/$host/$port' 2>/dev/null" +} + +# DNS check +check_dns() { + local domain="$1" + local expected_ip="${2:-}" + + local result + result=$(retry_exec $MAX_RETRIES $RETRY_DELAY \ + dig +short "$domain" 2>/dev/null | head -1) + + if [[ -n "$result" ]]; then + if [[ -z "$expected_ip" ]] || [[ "$result" == "$expected_ip" ]]; then + echo "OK|$result" + return 0 + fi + fi + + echo "FAIL|$result" + return 1 +} + +# SSL certificate check +check_ssl() { + local domain="$1" + local warn_days="${2:-30}" + + local expiry + expiry=$(echo | openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null | \ + openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) + + if [[ -n "$expiry" ]]; then + local expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$expiry" +%s 2>/dev/null) + local now=$(date +%s) + local days_left=$(( (expiry_epoch - now) / 86400 )) + + if [[ $days_left -lt 0 ]]; then + echo "EXPIRED|$days_left|$expiry" + return 1 + elif [[ $days_left -lt $warn_days ]]; then + echo "WARNING|$days_left|$expiry" + return 0 + else + echo "OK|$days_left|$expiry" + return 0 + fi + fi + + echo "ERROR|0|unknown" + return 1 +} + +# Disk space check +check_disk() { + local path="${1:-/}" + local threshold="${2:-90}" + + local usage=$(df -h "$path" 2>/dev/null | awk 'NR==2 {gsub(/%/,"",$5); print $5}') + + if [[ -n "$usage" ]]; then + if [[ $usage -ge $threshold ]]; then + echo "CRITICAL|$usage" + return 1 + elif [[ $usage -ge $((threshold - 10)) ]]; then + echo "WARNING|$usage" + return 0 + else + echo "OK|$usage" + return 0 + fi + fi + + echo "ERROR|0" + return 1 +} + +# Memory check +check_memory() { + local threshold="${1:-90}" + local usage=$(get_memory_usage 2>/dev/null || echo "0") + + if [[ $usage -ge $threshold ]]; then + echo "CRITICAL|$usage" + return 1 + elif [[ $usage -ge $((threshold - 10)) ]]; then + echo "WARNING|$usage" + return 0 + else + echo "OK|$usage" + return 0 + fi +} + +# CPU check +check_cpu() { + local threshold="${1:-90}" + local usage=$(get_cpu_usage 2>/dev/null || echo "0") + + if [[ $usage -ge $threshold ]]; then + echo "CRITICAL|$usage" + return 1 + elif [[ $usage -ge $((threshold - 10)) ]]; then + echo "WARNING|$usage" + return 0 + else + echo "OK|$usage" + return 0 + fi +} + +# Process check +check_process() { + local process_name="$1" + + if pgrep -x "$process_name" > /dev/null 2>&1; then + local pid=$(pgrep -x "$process_name" | head -1) + echo "OK|$pid" + return 0 + fi + + echo "FAIL|0" + return 1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# HEALTH CHECK DEFINITIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Define all health checks +declare -A HEALTH_CHECKS=( + # Format: "name|type|target|threshold|interval" + + # Network devices + ["lucidia_prime"]="ping|192.168.4.38||$CHECK_INTERVAL_FAST" + ["blackroad_pi"]="ping|192.168.4.64||$CHECK_INTERVAL_FAST" + ["lucidia_alt"]="ping|192.168.4.99||$CHECK_INTERVAL_FAST" + ["codex_vps"]="ping|159.65.43.12||$CHECK_INTERVAL_FAST" + + # APIs + ["github_api"]="http|https://api.github.com|200|$CHECK_INTERVAL_NORMAL" + ["cloudflare_api"]="http|https://api.cloudflare.com/client/v4|200|$CHECK_INTERVAL_NORMAL" + ["coingecko_api"]="http|https://api.coingecko.com/api/v3/ping|200|$CHECK_INTERVAL_NORMAL" + + # SSL Certificates + ["github_ssl"]="ssl|github.com|30|$CHECK_INTERVAL_SLOW" + ["cloudflare_ssl"]="ssl|cloudflare.com|30|$CHECK_INTERVAL_SLOW" + + # System resources + ["local_cpu"]="cpu||80|$CHECK_INTERVAL_FAST" + ["local_memory"]="memory||85|$CHECK_INTERVAL_FAST" + ["local_disk"]="disk|/|90|$CHECK_INTERVAL_NORMAL" +) + +#─────────────────────────────────────────────────────────────────────────────── +# CHECK EXECUTION +#─────────────────────────────────────────────────────────────────────────────── + +# Run a single health check +run_check() { + local name="$1" + local config="${HEALTH_CHECKS[$name]}" + + [[ -z "$config" ]] && return 1 + + IFS='|' read -r type target threshold interval <<< "$config" + + local result + local status="unknown" + local details="" + + case "$type" in + ping) + if result=$(check_ping "$target"); then + status="healthy" + else + status="unhealthy" + fi + ;; + http) + result=$(check_http "$target" "${threshold:-200}") + IFS='|' read -r stat code time size <<< "$result" + if [[ "$stat" == "OK" ]]; then + status="healthy" + details="HTTP $code, ${time}s" + else + status="unhealthy" + details="HTTP $code" + fi + ;; + ssl) + result=$(check_ssl "$target" "${threshold:-30}") + IFS='|' read -r stat days expiry <<< "$result" + case "$stat" in + OK) status="healthy"; details="$days days left" ;; + WARNING) status="degraded"; details="$days days left" ;; + EXPIRED) status="unhealthy"; details="Expired!" ;; + *) status="unknown"; details="Check failed" ;; + esac + ;; + cpu) + result=$(check_cpu "${threshold:-80}") + IFS='|' read -r stat usage <<< "$result" + case "$stat" in + OK) status="healthy"; details="${usage}%" ;; + WARNING) status="degraded"; details="${usage}%" ;; + CRITICAL) status="unhealthy"; details="${usage}%" ;; + esac + ;; + memory) + result=$(check_memory "${threshold:-85}") + IFS='|' read -r stat usage <<< "$result" + case "$stat" in + OK) status="healthy"; details="${usage}%" ;; + WARNING) status="degraded"; details="${usage}%" ;; + CRITICAL) status="unhealthy"; details="${usage}%" ;; + esac + ;; + disk) + result=$(check_disk "$target" "${threshold:-90}") + IFS='|' read -r stat usage <<< "$result" + case "$stat" in + OK) status="healthy"; details="${usage}%" ;; + WARNING) status="degraded"; details="${usage}%" ;; + CRITICAL) status="unhealthy"; details="${usage}%" ;; + esac + ;; + esac + + # Update state + local prev_status="${HEALTH_STATUS[$name]:-unknown}" + HEALTH_STATUS[$name]="$status" + LAST_CHECK_TIME[$name]=$(date +%s) + + # Handle state transitions + if [[ "$status" != "$prev_status" ]]; then + handle_state_change "$name" "$prev_status" "$status" "$details" + fi + + # Track failures + if [[ "$status" == "unhealthy" ]]; then + FAILURE_COUNT[$name]=$((${FAILURE_COUNT[$name]:-0} + 1)) + else + FAILURE_COUNT[$name]=0 + fi + + echo "$name|$status|$details" + log_debug "Health check: $name = $status ($details)" +} + +# Handle state changes +handle_state_change() { + local name="$1" + local old_status="$2" + local new_status="$3" + local details="$4" + + local now=$(date +%s) + local last_alert="${LAST_ALERT_TIME[$name]:-0}" + + # Rate limit alerts (min 5 minutes between alerts for same check) + if [[ $((now - last_alert)) -lt 300 ]]; then + return + fi + + case "$new_status" in + unhealthy) + notify_error "Health Check Failed" "$name is DOWN: $details" + LAST_ALERT_TIME[$name]=$now + ;; + degraded) + notify_warning "Health Check Degraded" "$name is degraded: $details" + LAST_ALERT_TIME[$name]=$now + ;; + healthy) + if [[ "$old_status" == "unhealthy" ]]; then + notify_success "Health Check Recovered" "$name is back UP: $details" + LAST_ALERT_TIME[$name]=$now + fi + ;; + esac + + # Log state change + printf "[%s] %s: %s -> %s (%s)\n" \ + "$(date '+%Y-%m-%d %H:%M:%S')" "$name" "$old_status" "$new_status" "$details" \ + >> "$HEALTH_LOG" +} + +# Run all health checks +run_all_checks() { + local results=() + + for name in "${!HEALTH_CHECKS[@]}"; do + results+=("$(run_check "$name")") + done + + printf '%s\n' "${results[@]}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# HEALTH DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_health_dashboard() { + clear_screen + cursor_hide + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🏥 BLACKROAD HEALTH MONITOR ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Summary + local healthy=0 degraded=0 unhealthy=0 unknown=0 + + for name in "${!HEALTH_CHECKS[@]}"; do + case "${HEALTH_STATUS[$name]:-unknown}" in + healthy) ((healthy++)) ;; + degraded) ((degraded++)) ;; + unhealthy) ((unhealthy++)) ;; + *) ((unknown++)) ;; + esac + done + + local total=$((healthy + degraded + unhealthy + unknown)) + + printf " ${BR_GREEN}●${RST} Healthy: %-5d" "$healthy" + printf "${BR_YELLOW}●${RST} Degraded: %-5d" "$degraded" + printf "${BR_RED}●${RST} Unhealthy: %-5d" "$unhealthy" + printf "${TEXT_MUTED}●${RST} Unknown: %-5d\n\n" "$unknown" + + # Health checks table + printf "${TEXT_MUTED}┌────────────────────┬───────────┬────────────────────────────────┐${RST}\n" + printf "${TEXT_MUTED}│${RST} ${BOLD}%-18s${RST} ${TEXT_MUTED}│${RST} ${BOLD}%-9s${RST} ${TEXT_MUTED}│${RST} ${BOLD}%-30s${RST} ${TEXT_MUTED}│${RST}\n" "Check" "Status" "Details" + printf "${TEXT_MUTED}├────────────────────┼───────────┼────────────────────────────────┤${RST}\n" + + for name in $(echo "${!HEALTH_CHECKS[@]}" | tr ' ' '\n' | sort); do + local status="${HEALTH_STATUS[$name]:-unknown}" + local last_time="${LAST_CHECK_TIME[$name]:-0}" + local failures="${FAILURE_COUNT[$name]:-0}" + + local status_color="$TEXT_MUTED" + local status_icon="○" + + case "$status" in + healthy) status_color="$BR_GREEN"; status_icon="●" ;; + degraded) status_color="$BR_YELLOW"; status_icon="◐" ;; + unhealthy) status_color="$BR_RED"; status_icon="●" ;; + esac + + local ago="" + if [[ $last_time -gt 0 ]]; then + ago=$(time_ago "$(($(date +%s) - last_time))") + fi + + local details="checked $ago" + [[ $failures -gt 0 ]] && details+=" (${failures} failures)" + + printf "${TEXT_MUTED}│${RST} %-18s ${TEXT_MUTED}│${RST} ${status_color}${status_icon} %-7s${RST} ${TEXT_MUTED}│${RST} %-30s ${TEXT_MUTED}│${RST}\n" \ + "$name" "$status" "$details" + done + + printf "${TEXT_MUTED}└────────────────────┴───────────┴────────────────────────────────┘${RST}\n" + + printf "\n${TEXT_SECONDARY}[r] Refresh [a] Run all checks [q] Quit${RST}\n" +} + +# Interactive health dashboard +health_dashboard() { + while true; do + render_health_dashboard + + if read -rsn1 -t 5 key 2>/dev/null; then + case "$key" in + r|R) continue ;; + a|A) + printf "\n${BR_CYAN}Running all health checks...${RST}\n" + run_all_checks + sleep 2 + ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONTINUOUS MONITORING +#─────────────────────────────────────────────────────────────────────────────── + +# Background monitor daemon +monitor_daemon() { + log_info "Starting health monitor daemon..." + + while true; do + for name in "${!HEALTH_CHECKS[@]}"; do + local config="${HEALTH_CHECKS[$name]}" + IFS='|' read -r type target threshold interval <<< "$config" + + local last_time="${LAST_CHECK_TIME[$name]:-0}" + local now=$(date +%s) + + if [[ $((now - last_time)) -ge ${interval:-60} ]]; then + run_check "$name" >/dev/null 2>&1 & + fi + done + + sleep 5 + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) health_dashboard ;; + check) run_check "$2" ;; + all) run_all_checks ;; + daemon) monitor_daemon ;; + ping) check_ping "$2" ;; + http) check_http "$2" "$3" ;; + ssl) check_ssl "$2" "$3" ;; + disk) check_disk "$2" "$3" ;; + memory) check_memory "$2" ;; + cpu) check_cpu "$2" ;; + *) + printf "Usage: %s [dashboard|check|all|daemon|ping|http|ssl|disk|memory|cpu]\n" "$0" + printf " %s check github_api\n" "$0" + printf " %s http https://example.com 200\n" "$0" + printf " %s ssl example.com 30\n" "$0" + ;; + esac +fi diff --git a/http-server.sh b/http-server.sh new file mode 100644 index 0000000..6e7d190 --- /dev/null +++ b/http-server.sh @@ -0,0 +1,496 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██╗████████╗████████╗██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ +# ██║ ██║╚══██╔══╝╚══██╔══╝██╔══██╗ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ +# ███████║ ██║ ██║ ██████╔╝ ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ +# ██╔══██║ ██║ ██║ ██╔═══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ +# ██║ ██║ ██║ ██║ ██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ +# ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD HTTP DASHBOARD SERVER v3.0 +# Real-time Web Dashboard with Live Metrics API +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +HTTP_PORT="${HTTP_PORT:-8888}" +HTTP_HOST="${HTTP_HOST:-0.0.0.0}" +HTTP_ROOT="${SCRIPT_DIR}/www" +HTTP_LOG="${BLACKROAD_LOGS:-/tmp}/http-server.log" + +mkdir -p "$HTTP_ROOT" 2>/dev/null + +#─────────────────────────────────────────────────────────────────────────────── +# API ENDPOINTS +#─────────────────────────────────────────────────────────────────────────────── + +# Get system metrics +api_metrics() { + local cpu=$(get_cpu_usage 2>/dev/null || echo "0") + local mem=$(get_memory_usage 2>/dev/null || echo "0") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "0") + local uptime=$(get_uptime_seconds 2>/dev/null || echo "0") + local load=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}' || echo "0") + + cat << EOF +{ + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "cpu": { + "usage": $cpu, + "cores": $(nproc 2>/dev/null || echo 1) + }, + "memory": { + "usage": $mem, + "total_mb": $(free -m 2>/dev/null | awk '/Mem:/ {print $2}' || echo 0), + "used_mb": $(free -m 2>/dev/null | awk '/Mem:/ {print $3}' || echo 0) + }, + "disk": { + "usage": $disk, + "path": "/" + }, + "system": { + "uptime_seconds": $uptime, + "load_average": $load, + "hostname": "$(hostname)", + "os": "$(uname -s)", + "kernel": "$(uname -r)" + } +} +EOF +} + +# Get network status +api_network() { + local devices=( + '{"name":"Lucidia Prime","host":"192.168.4.38"}' + '{"name":"BlackRoad Pi","host":"192.168.4.64"}' + '{"name":"Codex VPS","host":"159.65.43.12"}' + ) + + printf '{"timestamp":"%s","devices":[' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + local first=true + for dev in "${devices[@]}"; do + local host=$(echo "$dev" | grep -oE '"host":"[^"]+"' | cut -d'"' -f4) + local name=$(echo "$dev" | grep -oE '"name":"[^"]+"' | cut -d'"' -f4) + + local status="offline" + ping -c 1 -W 1 "$host" &>/dev/null && status="online" + + [[ "$first" != "true" ]] && printf "," + first=false + printf '{"name":"%s","host":"%s","status":"%s"}' "$name" "$host" "$status" + done + + printf ']}' +} + +# Get crypto prices +api_crypto() { + local btc=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local eth=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + local sol=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true" 2>/dev/null) + + cat << EOF +{ + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "prices": { + "bitcoin": $(echo "$btc" | grep -oE '"bitcoin":\{[^}]+\}' || echo '{"usd":0}'), + "ethereum": $(echo "$eth" | grep -oE '"ethereum":\{[^}]+\}' || echo '{"usd":0}'), + "solana": $(echo "$sol" | grep -oE '"solana":\{[^}]+\}' || echo '{"usd":0}') + } +} +EOF +} + +# Get all data +api_dashboard() { + local metrics=$(api_metrics) + local network=$(api_network) + + cat << EOF +{ + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "metrics": $metrics, + "network": $network, + "version": "3.0.0" +} +EOF +} + +#─────────────────────────────────────────────────────────────────────────────── +# HTML TEMPLATES +#─────────────────────────────────────────────────────────────────────────────── + +generate_dashboard_html() { + cat << 'HTML' + + + + + + BlackRoad Dashboard + + + +
+
BLACKROAD DASHBOARD
+ + Loading... + +
+
+

CPU

+
+ Usage + --% +
+
+
+
+
+ +
+

Memory

+
+ Usage + --% +
+
+
+
+
+ +
+

Disk

+
+ Usage + --% +
+
+
+
+
+ +
+

System

+
+ Hostname + -- +
+
+ Uptime + -- +
+
+ Load + -- +
+
+ +
+

Network Devices

+
+
+
+
+ + + + +HTML +} + +#─────────────────────────────────────────────────────────────────────────────── +# HTTP SERVER +#─────────────────────────────────────────────────────────────────────────────── + +# Parse HTTP request +parse_request() { + local request="$1" + local method=$(echo "$request" | head -1 | awk '{print $1}') + local path=$(echo "$request" | head -1 | awk '{print $2}') + + echo "$method $path" +} + +# Send HTTP response +send_response() { + local status="$1" + local content_type="$2" + local body="$3" + local content_length=${#body} + + printf "HTTP/1.1 %s\r\n" "$status" + printf "Content-Type: %s\r\n" "$content_type" + printf "Content-Length: %d\r\n" "$content_length" + printf "Access-Control-Allow-Origin: *\r\n" + printf "Connection: close\r\n" + printf "\r\n" + printf "%s" "$body" +} + +# Handle request +handle_request() { + local request="" + local line + + # Read request headers + while IFS= read -r line; do + line="${line%$'\r'}" + [[ -z "$line" ]] && break + request+="$line"$'\n' + done + + local method_path=$(parse_request "$request") + local method="${method_path%% *}" + local path="${method_path#* }" + + # Log request + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $method $path" >> "$HTTP_LOG" + + # Route request + case "$path" in + /|/index.html) + local html=$(generate_dashboard_html) + send_response "200 OK" "text/html; charset=utf-8" "$html" + ;; + /api/metrics) + local json=$(api_metrics) + send_response "200 OK" "application/json" "$json" + ;; + /api/network) + local json=$(api_network) + send_response "200 OK" "application/json" "$json" + ;; + /api/crypto) + local json=$(api_crypto) + send_response "200 OK" "application/json" "$json" + ;; + /api/dashboard) + local json=$(api_dashboard) + send_response "200 OK" "application/json" "$json" + ;; + /health) + send_response "200 OK" "application/json" '{"status":"healthy"}' + ;; + *) + send_response "404 Not Found" "application/json" '{"error":"Not Found"}' + ;; + esac +} + +# Start server using netcat +start_server_nc() { + log_info "Starting HTTP server on http://$HTTP_HOST:$HTTP_PORT" + printf "${BR_GREEN}${BOLD}BlackRoad HTTP Server v3.0${RST}\n" + printf "${TEXT_SECONDARY}Listening on http://%s:%s${RST}\n\n" "$HTTP_HOST" "$HTTP_PORT" + printf "${TEXT_MUTED}Endpoints:${RST}\n" + printf " ${BR_CYAN}/ ${TEXT_SECONDARY}- Dashboard UI${RST}\n" + printf " ${BR_CYAN}/api/metrics ${TEXT_SECONDARY}- System metrics${RST}\n" + printf " ${BR_CYAN}/api/network ${TEXT_SECONDARY}- Network status${RST}\n" + printf " ${BR_CYAN}/api/crypto ${TEXT_SECONDARY}- Crypto prices${RST}\n" + printf " ${BR_CYAN}/api/dashboard ${TEXT_SECONDARY}- All data${RST}\n" + printf " ${BR_CYAN}/health ${TEXT_SECONDARY}- Health check${RST}\n" + printf "\n${TEXT_MUTED}Press Ctrl+C to stop${RST}\n\n" + + while true; do + # Use netcat or socat + if command -v nc &>/dev/null; then + nc -l -p "$HTTP_PORT" -c "$(realpath "$0") handle" 2>/dev/null || \ + nc -l "$HTTP_PORT" -e "$(realpath "$0") handle" 2>/dev/null || \ + { + # Fallback: simple nc without -c/-e + { handle_request | nc -l -p "$HTTP_PORT" -q 1; } 2>/dev/null + } + elif command -v socat &>/dev/null; then + socat TCP-LISTEN:"$HTTP_PORT",reuseaddr,fork EXEC:"$(realpath "$0") handle" + else + log_error "Neither nc nor socat found. Install netcat or socat." + exit 1 + fi + done +} + +# Simple bash-only server (slower but more compatible) +start_server_bash() { + log_info "Starting HTTP server (bash mode) on port $HTTP_PORT" + + # Create named pipe + local pipe="/tmp/blackroad_http_$$" + mkfifo "$pipe" 2>/dev/null + + trap "rm -f $pipe; exit" INT TERM EXIT + + while true; do + cat "$pipe" | handle_request | nc -l -p "$HTTP_PORT" > "$pipe" 2>/dev/null + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-start}" in + start) + trap "echo; log_info 'Server stopped'; exit 0" INT TERM + start_server_nc + ;; + handle) + handle_request + ;; + api) + case "$2" in + metrics) api_metrics ;; + network) api_network ;; + crypto) api_crypto ;; + dashboard) api_dashboard ;; + esac + ;; + *) + printf "Usage: %s [start|api ]\n" "$0" + printf " %s start # Start HTTP server\n" "$0" + printf " %s api metrics # Get metrics JSON\n" "$0" + ;; + esac +fi diff --git a/keyboard-shortcuts.sh b/keyboard-shortcuts.sh new file mode 100644 index 0000000..a4d5c25 --- /dev/null +++ b/keyboard-shortcuts.sh @@ -0,0 +1,437 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██╗███████╗██╗ ██╗███████╗ +# ██║ ██╔╝██╔════╝╚██╗ ██╔╝██╔════╝ +# █████╔╝ █████╗ ╚████╔╝ ███████╗ +# ██╔═██╗ ██╔══╝ ╚██╔╝ ╚════██║ +# ██║ ██╗███████╗ ██║ ███████║ +# ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD KEYBOARD SHORTCUTS SYSTEM v2.0 +# Unified keybindings with help overlay and customization +#═══════════════════════════════════════════════════════════════════════════════ + +# Source core library +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +SHORTCUTS_CONFIG="${BLACKROAD_CONFIG:-$HOME/.blackroad-dashboards}/shortcuts.json" + +# Default keyboard shortcuts +declare -A DEFAULT_SHORTCUTS=( + # Navigation + ["q"]="quit|Quit/Exit" + ["Q"]="quit_force|Force Quit" + ["h"]="help|Show Help" + ["?"]="help|Show Help" + + # Actions + ["r"]="refresh|Refresh Data" + ["R"]="refresh_full|Full Refresh" + ["e"]="export|Export Data" + ["E"]="export_all|Export All Formats" + + # Navigation + ["j"]="down|Move Down" + ["k"]="up|Move Up" + ["l"]="right|Move Right" + ["h"]="left|Move Left" + ["g"]="top|Go to Top" + ["G"]="bottom|Go to Bottom" + + # Arrow keys (escape sequences) + ["\e[A"]="up|Move Up" + ["\e[B"]="down|Move Down" + ["\e[C"]="right|Move Right" + ["\e[D"]="left|Move Left" + + # Tabs/Panels + ["1"]="tab_1|Tab 1" + ["2"]="tab_2|Tab 2" + ["3"]="tab_3|Tab 3" + ["4"]="tab_4|Tab 4" + ["5"]="tab_5|Tab 5" + ["Tab"]="next_tab|Next Tab" + + # Views + ["s"]="ssh|SSH Connect" + ["t"]="themes|Theme Selector" + ["n"]="notifications|Notifications" + ["a"]="analytics|Analytics View" + ["m"]="metrics|System Metrics" + ["d"]="details|Show Details" + ["c"]="config|Configuration" + + # Search/Filter + ["/"]="search|Search" + ["f"]="filter|Filter" + ["x"]="clear_filter|Clear Filter" + + # Toggle + ["Space"]="select|Select/Toggle" + ["Enter"]="confirm|Confirm/Open" + ["Escape"]="cancel|Cancel/Back" + + # Special + ["!"]="command|Command Mode" + [":"]="command|Command Mode" + ["p"]="pause|Pause Updates" + ["z"]="zoom|Zoom Panel" +) + +# Current active shortcuts (can be customized) +declare -A SHORTCUTS +for key in "${!DEFAULT_SHORTCUTS[@]}"; do + SHORTCUTS[$key]="${DEFAULT_SHORTCUTS[$key]}" +done + +#─────────────────────────────────────────────────────────────────────────────── +# KEY READING +#─────────────────────────────────────────────────────────────────────────────── + +# Read a single key with timeout +read_key() { + local timeout="${1:-0}" + local key="" + + # Read first character + if [[ $timeout -gt 0 ]]; then + read -rsn1 -t "$timeout" key 2>/dev/null || return 1 + else + read -rsn1 key 2>/dev/null || return 1 + fi + + # Handle escape sequences + if [[ "$key" == $'\e' ]]; then + local seq="" + read -rsn1 -t 0.01 seq 2>/dev/null + if [[ -n "$seq" ]]; then + read -rsn1 -t 0.01 seq2 2>/dev/null + key="$key$seq$seq2" + + # Extended sequences (F-keys, etc.) + if [[ "$seq2" =~ [0-9] ]]; then + local more="" + read -rsn1 -t 0.01 more 2>/dev/null + key="$key$more" + fi + fi + fi + + echo "$key" +} + +# Convert key to readable name +key_name() { + local key="$1" + + case "$key" in + $'\e') echo "Escape" ;; + $'\e[A') echo "Up" ;; + $'\e[B') echo "Down" ;; + $'\e[C') echo "Right" ;; + $'\e[D') echo "Left" ;; + $'\e[H') echo "Home" ;; + $'\e[F') echo "End" ;; + $'\e[5~') echo "PageUp" ;; + $'\e[6~') echo "PageDown" ;; + $'\e[2~') echo "Insert" ;; + $'\e[3~') echo "Delete" ;; + $'\eOP') echo "F1" ;; + $'\eOQ') echo "F2" ;; + $'\eOR') echo "F3" ;; + $'\eOS') echo "F4" ;; + $'\e[15~') echo "F5" ;; + $'\e[17~') echo "F6" ;; + $'\e[18~') echo "F7" ;; + $'\e[19~') echo "F8" ;; + $'\e[20~') echo "F9" ;; + $'\e[21~') echo "F10" ;; + $'\e[23~') echo "F11" ;; + $'\e[24~') echo "F12" ;; + $'\t') echo "Tab" ;; + $'\n') echo "Enter" ;; + ' ') echo "Space" ;; + $'\177') echo "Backspace" ;; + *) echo "$key" ;; + esac +} + +# Get action for key +get_action() { + local key="$1" + local shortcut="${SHORTCUTS[$key]:-}" + + if [[ -n "$shortcut" ]]; then + echo "${shortcut%%|*}" + fi +} + +# Get description for key +get_description() { + local key="$1" + local shortcut="${SHORTCUTS[$key]:-}" + + if [[ -n "$shortcut" ]]; then + echo "${shortcut#*|}" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# HELP OVERLAY +#─────────────────────────────────────────────────────────────────────────────── + +# Show help overlay +show_help_overlay() { + local width=70 + local height=25 + + # Calculate position (centered) + get_term_size + local start_row=$(( (TERM_ROWS - height) / 2 )) + local start_col=$(( (TERM_COLS - width) / 2 )) + + # Draw overlay background + for ((row=0; row/dev/null + +#─────────────────────────────────────────────────────────────────────────────── +# CACHE KEY GENERATION +#─────────────────────────────────────────────────────────────────────────────── + +# Generate cache key from input (URL, command, etc.) +cache_key() { + local input="$1" + local namespace="${2:-default}" + + # Create hash of input + local hash + if command -v md5sum &>/dev/null; then + hash=$(echo -n "$input" | md5sum | cut -d' ' -f1) + elif command -v md5 &>/dev/null; then + hash=$(echo -n "$input" | md5) + else + # Fallback: simple hash + hash=$(echo -n "$input" | cksum | cut -d' ' -f1) + fi + + echo "${namespace}_${hash}" +} + +# Get cache file path for key +cache_path() { + local key="$1" + echo "$CACHE_DIR/${key}.cache" +} + +# Get metadata file path +cache_meta_path() { + local key="$1" + echo "$CACHE_DIR/${key}.meta" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CORE CACHE OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Set cache value +cache_set() { + local key="$1" + local value="$2" + local ttl="${3:-$CACHE_DEFAULT_TTL}" + + local cache_file=$(cache_path "$key") + local meta_file=$(cache_meta_path "$key") + local now=$(date +%s) + local expires=$((now + ttl)) + + # Handle TTL presets + if [[ -n "${CACHE_TTL_PRESETS[$ttl]}" ]]; then + expires=$((now + ${CACHE_TTL_PRESETS[$ttl]})) + [[ "${CACHE_TTL_PRESETS[$ttl]}" == "0" ]] && expires=0 + fi + + # Write metadata + cat > "$meta_file" << EOF +{ + "key": "$key", + "created": $now, + "expires": $expires, + "ttl": $ttl, + "size": ${#value}, + "compressed": $CACHE_COMPRESSION, + "hits": 0 +} +EOF + + # Write data (optionally compressed) + if [[ "$CACHE_COMPRESSION" == "1" ]] && command -v gzip &>/dev/null; then + echo "$value" | gzip > "$cache_file" + else + echo "$value" > "$cache_file" + fi + + log_debug "Cache SET: $key (TTL: ${ttl}s, Size: ${#value} bytes)" + return 0 +} + +# Get cache value +cache_get() { + local key="$1" + local cache_file=$(cache_path "$key") + local meta_file=$(cache_meta_path "$key") + + # Check if cache exists + if [[ ! -f "$cache_file" ]] || [[ ! -f "$meta_file" ]]; then + log_debug "Cache MISS: $key (not found)" + return 1 + fi + + # Check expiration + local now=$(date +%s) + local expires=$(grep -o '"expires": [0-9]*' "$meta_file" | grep -o '[0-9]*') + + if [[ "$expires" != "0" ]] && [[ $now -gt $expires ]]; then + log_debug "Cache MISS: $key (expired)" + rm -f "$cache_file" "$meta_file" 2>/dev/null + return 1 + fi + + # Update hit count + if command -v jq &>/dev/null; then + local hits=$(jq '.hits' "$meta_file" 2>/dev/null || echo "0") + jq ".hits = $((hits + 1))" "$meta_file" > "${meta_file}.tmp" 2>/dev/null + mv "${meta_file}.tmp" "$meta_file" 2>/dev/null + fi + + # Read data (decompress if needed) + local compressed=$(grep -o '"compressed": [01]' "$meta_file" | grep -o '[01]') + if [[ "$compressed" == "1" ]] && command -v gunzip &>/dev/null; then + gunzip -c "$cache_file" + else + cat "$cache_file" + fi + + log_debug "Cache HIT: $key" + return 0 +} + +# Check if cache key exists and is valid +cache_exists() { + local key="$1" + local cache_file=$(cache_path "$key") + local meta_file=$(cache_meta_path "$key") + + [[ ! -f "$cache_file" ]] && return 1 + [[ ! -f "$meta_file" ]] && return 1 + + local now=$(date +%s) + local expires=$(grep -o '"expires": [0-9]*' "$meta_file" | grep -o '[0-9]*') + + [[ "$expires" != "0" ]] && [[ $now -gt $expires ]] && return 1 + + return 0 +} + +# Delete cache key +cache_delete() { + local key="$1" + local cache_file=$(cache_path "$key") + local meta_file=$(cache_meta_path "$key") + + rm -f "$cache_file" "$meta_file" 2>/dev/null + log_debug "Cache DELETE: $key" + return 0 +} + +# Get remaining TTL for key +cache_ttl() { + local key="$1" + local meta_file=$(cache_meta_path "$key") + + [[ ! -f "$meta_file" ]] && echo "-1" && return 1 + + local now=$(date +%s) + local expires=$(grep -o '"expires": [0-9]*' "$meta_file" | grep -o '[0-9]*') + + if [[ "$expires" == "0" ]]; then + echo "infinite" + else + local remaining=$((expires - now)) + [[ $remaining -lt 0 ]] && remaining=0 + echo "$remaining" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# CACHED FUNCTION WRAPPERS +#─────────────────────────────────────────────────────────────────────────────── + +# Cache a curl request +cache_curl() { + local url="$1" + local ttl="${2:-$CACHE_DEFAULT_TTL}" + shift 2 + local curl_args=("$@") + + local key=$(cache_key "$url ${curl_args[*]}" "curl") + + # Try to get from cache + local cached + if cached=$(cache_get "$key"); then + echo "$cached" + return 0 + fi + + # Fetch and cache + local result + if result=$(curl -s "${curl_args[@]}" "$url" 2>/dev/null); then + cache_set "$key" "$result" "$ttl" + echo "$result" + return 0 + fi + + return 1 +} + +# Cache any command output +cache_command() { + local ttl="$1" + shift + local cmd=("$@") + + local key=$(cache_key "${cmd[*]}" "cmd") + + # Try to get from cache + local cached + if cached=$(cache_get "$key"); then + echo "$cached" + return 0 + fi + + # Execute and cache + local result + if result=$("${cmd[@]}" 2>/dev/null); then + cache_set "$key" "$result" "$ttl" + echo "$result" + return 0 + fi + + return 1 +} + +# Memoize a function (cache its output based on args) +memoize() { + local func="$1" + local ttl="${2:-$CACHE_DEFAULT_TTL}" + shift 2 + local args=("$@") + + local key=$(cache_key "$func ${args[*]}" "memo") + + # Try cache + local cached + if cached=$(cache_get "$key"); then + echo "$cached" + return 0 + fi + + # Call function and cache + local result + if result=$("$func" "${args[@]}"); then + cache_set "$key" "$result" "$ttl" + echo "$result" + return 0 + fi + + return 1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# NAMESPACE OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Clear all cache for a namespace +cache_clear_namespace() { + local namespace="$1" + local count=0 + + for file in "$CACHE_DIR/${namespace}_"*.cache; do + [[ -f "$file" ]] && rm -f "$file" "${file%.cache}.meta" && ((count++)) + done + + log_info "Cleared $count cache entries for namespace: $namespace" + return 0 +} + +# List all keys in namespace +cache_list_namespace() { + local namespace="$1" + + for file in "$CACHE_DIR/${namespace}_"*.meta; do + [[ -f "$file" ]] && basename "$file" .meta + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# CACHE MAINTENANCE +#─────────────────────────────────────────────────────────────────────────────── + +# Clean expired entries +cache_cleanup() { + local now=$(date +%s) + local count=0 + + for meta_file in "$CACHE_DIR"/*.meta; do + [[ ! -f "$meta_file" ]] && continue + + local expires=$(grep -o '"expires": [0-9]*' "$meta_file" | grep -o '[0-9]*') + + if [[ "$expires" != "0" ]] && [[ $now -gt $expires ]]; then + local cache_file="${meta_file%.meta}.cache" + rm -f "$cache_file" "$meta_file" 2>/dev/null + ((count++)) + fi + done + + log_info "Cache cleanup: removed $count expired entries" + return 0 +} + +# Clear all cache +cache_clear_all() { + rm -f "$CACHE_DIR"/*.cache "$CACHE_DIR"/*.meta 2>/dev/null + log_info "Cache cleared completely" + return 0 +} + +# Get cache statistics +cache_stats() { + local total_files=0 + local total_size=0 + local total_hits=0 + local expired=0 + local now=$(date +%s) + + for meta_file in "$CACHE_DIR"/*.meta; do + [[ ! -f "$meta_file" ]] && continue + + ((total_files++)) + + local cache_file="${meta_file%.meta}.cache" + [[ -f "$cache_file" ]] && total_size=$((total_size + $(stat -f%z "$cache_file" 2>/dev/null || stat -c%s "$cache_file" 2>/dev/null || echo 0))) + + local hits=$(grep -o '"hits": [0-9]*' "$meta_file" | grep -o '[0-9]*') + total_hits=$((total_hits + ${hits:-0})) + + local expires=$(grep -o '"expires": [0-9]*' "$meta_file" | grep -o '[0-9]*') + [[ "$expires" != "0" ]] && [[ $now -gt $expires ]] && ((expired++)) + done + + cat << EOF +{ + "total_entries": $total_files, + "total_size_bytes": $total_size, + "total_size_human": "$(format_bytes $total_size)", + "total_hits": $total_hits, + "expired_entries": $expired, + "cache_directory": "$CACHE_DIR", + "max_size_bytes": $CACHE_MAX_SIZE, + "compression_enabled": $CACHE_COMPRESSION +} +EOF +} + +# Prune cache if over size limit +cache_prune() { + local current_size=0 + + for cache_file in "$CACHE_DIR"/*.cache; do + [[ -f "$cache_file" ]] && current_size=$((current_size + $(stat -f%z "$cache_file" 2>/dev/null || stat -c%s "$cache_file" 2>/dev/null || echo 0))) + done + + if [[ $current_size -gt $CACHE_MAX_SIZE ]]; then + log_warn "Cache size ($current_size) exceeds limit ($CACHE_MAX_SIZE), pruning..." + + # Remove oldest files first + ls -t "$CACHE_DIR"/*.cache 2>/dev/null | tail -n +$((CACHE_MAX_SIZE / 10000)) | while read file; do + rm -f "$file" "${file%.cache}.meta" 2>/dev/null + current_size=$((current_size - $(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo 0))) + [[ $current_size -lt $CACHE_MAX_SIZE ]] && break + done + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# CACHE WARMING +#─────────────────────────────────────────────────────────────────────────────── + +# Warm cache with predefined URLs/commands +cache_warm() { + local -a urls=("$@") + local count=0 + + for url in "${urls[@]}"; do + cache_curl "$url" "standard" -H "Accept: application/json" & + ((count++)) + done + + wait + log_info "Cache warmed with $count entries" +} + +#─────────────────────────────────────────────────────────────────────────────── +# API-SPECIFIC CACHED FETCHERS +#─────────────────────────────────────────────────────────────────────────────── + +# Cached GitHub API call +cache_github() { + local endpoint="$1" + local ttl="${2:-standard}" + + local url="https://api.github.com/$endpoint" + local headers=(-H "Accept: application/vnd.github.v3+json") + + [[ -n "$GITHUB_TOKEN" ]] && headers+=(-H "Authorization: token $GITHUB_TOKEN") + + cache_curl "$url" "$ttl" "${headers[@]}" +} + +# Cached Cloudflare API call +cache_cloudflare() { + local endpoint="$1" + local ttl="${2:-standard}" + + [[ -z "$CLOUDFLARE_TOKEN" ]] && return 1 + + local url="https://api.cloudflare.com/client/v4/$endpoint" + + cache_curl "$url" "$ttl" \ + -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \ + -H "Content-Type: application/json" +} + +# Cached crypto price (fast TTL) +cache_crypto_price() { + local coin="${1:-bitcoin}" + local ttl="${2:-fast}" + + cache_curl "https://api.coingecko.com/api/v3/simple/price?ids=$coin&vs_currencies=usd&include_24hr_change=true" "$ttl" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CACHE DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +# Display cache dashboard +cache_dashboard() { + clear_screen + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════╗\n" + printf "║ 📦 BLACKROAD CACHE DASHBOARD ║\n" + printf "╚══════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + local stats=$(cache_stats) + + if command -v jq &>/dev/null; then + local entries=$(echo "$stats" | jq -r '.total_entries') + local size=$(echo "$stats" | jq -r '.total_size_human') + local hits=$(echo "$stats" | jq -r '.total_hits') + local expired=$(echo "$stats" | jq -r '.expired_entries') + + printf "${TEXT_SECONDARY}┌─────────────────┬───────────────────┐${RST}\n" + printf "${TEXT_SECONDARY}│${RST} ${BR_ORANGE}Total Entries${RST} ${TEXT_SECONDARY}│${RST} %-17s ${TEXT_SECONDARY}│${RST}\n" "$entries" + printf "${TEXT_SECONDARY}│${RST} ${BR_GREEN}Cache Size${RST} ${TEXT_SECONDARY}│${RST} %-17s ${TEXT_SECONDARY}│${RST}\n" "$size" + printf "${TEXT_SECONDARY}│${RST} ${BR_CYAN}Total Hits${RST} ${TEXT_SECONDARY}│${RST} %-17s ${TEXT_SECONDARY}│${RST}\n" "$hits" + printf "${TEXT_SECONDARY}│${RST} ${BR_RED}Expired${RST} ${TEXT_SECONDARY}│${RST} %-17s ${TEXT_SECONDARY}│${RST}\n" "$expired" + printf "${TEXT_SECONDARY}└─────────────────┴───────────────────┘${RST}\n" + else + echo "$stats" + fi + + printf "\n${TEXT_MUTED}Cache directory: $CACHE_DIR${RST}\n" + printf "\n${TEXT_SECONDARY}Commands: [c]leanup [p]rune [x]clear all [q]uit${RST}\n" +} + +log_debug "BlackRoad Cache Library v2.0 loaded" diff --git a/lib-core.sh b/lib-core.sh new file mode 100644 index 0000000..3b35fab --- /dev/null +++ b/lib-core.sh @@ -0,0 +1,767 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ █████╗ ██████╗ +# ██╔══██╗ ██║ ██╔══██╗ ██╔════╝ ██║ ██╔╝ ██╔══██╗ ██╔═══██╗ ██╔══██╗ ██╔══██╗ +# ██████╔╝ ██║ ███████║ ██║ █████╔╝ ██████╔╝ ██║ ██║ ███████║ ██║ ██║ +# ██╔══██╗ ██║ ██╔══██║ ██║ ██╔═██╗ ██╔══██╗ ██║ ██║ ██╔══██║ ██║ ██║ +# ██████╔╝ ███████╗██║ ██║ ╚██████╗ ██║ ██╗ ██║ ██║ ╚██████╔╝ ██║ ██║ ██████╔╝ +# ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD CORE LIBRARY v2.0 +# Shared functions, utilities, and components for all dashboards +#═══════════════════════════════════════════════════════════════════════════════ + +# Prevent multiple inclusions +[[ -n "$BLACKROAD_CORE_LOADED" ]] && return 0 +export BLACKROAD_CORE_LOADED=1 + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION & PATHS +#─────────────────────────────────────────────────────────────────────────────── + +export BLACKROAD_HOME="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}" +export BLACKROAD_CACHE="$BLACKROAD_HOME/cache" +export BLACKROAD_LOGS="$BLACKROAD_HOME/logs" +export BLACKROAD_DATA="$BLACKROAD_HOME/data" +export BLACKROAD_CONFIG="$BLACKROAD_HOME/config" +export BLACKROAD_TEMP="/tmp/blackroad-$$" + +# Ensure directories exist +mkdir -p "$BLACKROAD_CACHE" "$BLACKROAD_LOGS" "$BLACKROAD_DATA" "$BLACKROAD_CONFIG" "$BLACKROAD_TEMP" 2>/dev/null + +#─────────────────────────────────────────────────────────────────────────────── +# CORE COLOR PALETTE - 24-bit RGB TrueColor +#─────────────────────────────────────────────────────────────────────────────── + +# Reset +export RST="\033[0m" +export BOLD="\033[1m" +export DIM="\033[2m" +export ITALIC="\033[3m" +export UNDERLINE="\033[4m" +export BLINK="\033[5m" +export REVERSE="\033[7m" + +# BlackRoad Brand Colors +export BR_ORANGE="\033[38;2;247;147;26m" +export BR_PINK="\033[38;2;233;30;140m" +export BR_PURPLE="\033[38;2;153;69;255m" +export BR_CYAN="\033[38;2;0;212;255m" +export BR_GREEN="\033[38;2;20;241;149m" +export BR_BLUE="\033[38;2;66;133;244m" +export BR_RED="\033[38;2;255;82;82m" +export BR_YELLOW="\033[38;2;255;215;0m" +export BR_WHITE="\033[38;2;255;255;255m" +export BR_GRAY="\033[38;2;128;128;128m" + +# Background variants +export BR_BG_ORANGE="\033[48;2;247;147;26m" +export BR_BG_PINK="\033[48;2;233;30;140m" +export BR_BG_PURPLE="\033[48;2;153;69;255m" +export BR_BG_CYAN="\033[48;2;0;212;255m" +export BR_BG_GREEN="\033[48;2;20;241;149m" +export BR_BG_DARK="\033[48;2;20;20;30m" + +# Semantic Colors +export COLOR_SUCCESS="$BR_GREEN" +export COLOR_ERROR="$BR_RED" +export COLOR_WARNING="$BR_YELLOW" +export COLOR_INFO="$BR_CYAN" +export COLOR_MUTED="$BR_GRAY" + +# Text Hierarchy +export TEXT_PRIMARY="$BR_WHITE" +export TEXT_SECONDARY="\033[38;2;180;180;180m" +export TEXT_MUTED="\033[38;2;100;100;100m" + +#─────────────────────────────────────────────────────────────────────────────── +# UNICODE SYMBOLS & BOX DRAWING +#─────────────────────────────────────────────────────────────────────────────── + +# Status Indicators +export SYM_ONLINE="◉" +export SYM_OFFLINE="○" +export SYM_CHECK="✓" +export SYM_CROSS="✗" +export SYM_WARNING="⚠" +export SYM_INFO="ℹ" +export SYM_ARROW="→" +export SYM_BULLET="•" +export SYM_STAR="★" +export SYM_LIGHTNING="⚡" +export SYM_FIRE="🔥" +export SYM_ROCKET="🚀" +export SYM_LOCK="🔒" +export SYM_UNLOCK="🔓" +export SYM_GEAR="⚙" +export SYM_CHART="📊" +export SYM_FOLDER="📁" +export SYM_FILE="📄" +export SYM_CLOCK="🕐" +export SYM_CALENDAR="📅" +export SYM_REFRESH="🔄" + +# Sparkline Characters +export SPARK_CHARS="▁▂▃▄▅▆▇█" + +# Box Drawing - Heavy +export BOX_TL="╔" +export BOX_TR="╗" +export BOX_BL="╚" +export BOX_BR="╝" +export BOX_H="═" +export BOX_V="║" +export BOX_LT="╠" +export BOX_RT="╣" +export BOX_TB="╦" +export BOX_BT="╩" +export BOX_X="╬" + +# Box Drawing - Light +export BOX_TL_L="┌" +export BOX_TR_L="┐" +export BOX_BL_L="└" +export BOX_BR_L="┘" +export BOX_H_L="─" +export BOX_V_L="│" +export BOX_LT_L="├" +export BOX_RT_L="┤" +export BOX_TB_L="┬" +export BOX_BT_L="┴" +export BOX_X_L="┼" + +# Progress Bar Characters +export PROG_FULL="█" +export PROG_HIGH="▓" +export PROG_MED="▒" +export PROG_LOW="░" +export PROG_EMPTY="░" + +#─────────────────────────────────────────────────────────────────────────────── +# TERMINAL UTILITIES +#─────────────────────────────────────────────────────────────────────────────── + +# Get terminal dimensions +get_term_size() { + TERM_COLS=$(tput cols 2>/dev/null || echo 80) + TERM_ROWS=$(tput lines 2>/dev/null || echo 24) + export TERM_COLS TERM_ROWS +} + +# Move cursor to position +cursor_to() { + printf "\033[%d;%dH" "$1" "$2" +} + +# Clear screen with optional mode +clear_screen() { + local mode="${1:-full}" + case "$mode" in + full) printf "\033[2J\033[H" ;; + line) printf "\033[2K\r" ;; + below) printf "\033[J" ;; + above) printf "\033[1J" ;; + esac +} + +# Hide/show cursor +cursor_hide() { printf "\033[?25l"; } +cursor_show() { printf "\033[?25h"; } + +# Save/restore cursor position +cursor_save() { printf "\033[s"; } +cursor_restore() { printf "\033[u"; } + +# Set terminal title +set_title() { + printf "\033]0;%s\007" "$1" +} + +#─────────────────────────────────────────────────────────────────────────────── +# UI RENDERING COMPONENTS +#─────────────────────────────────────────────────────────────────────────────── + +# Draw horizontal line +draw_line() { + local width="${1:-$TERM_COLS}" + local char="${2:-─}" + local color="${3:-$TEXT_MUTED}" + printf "${color}" + printf "%0.s${char}" $(seq 1 "$width") + printf "${RST}\n" +} + +# Draw box with title +draw_box() { + local title="$1" + local width="${2:-40}" + local color="${3:-$BR_CYAN}" + + # Top border + printf "${color}${BOX_TL}" + printf "%0.s${BOX_H}" $(seq 1 $((width - 2))) + printf "${BOX_TR}${RST}\n" + + # Title line + if [[ -n "$title" ]]; then + local title_len=${#title} + local padding=$(( (width - 4 - title_len) / 2 )) + printf "${color}${BOX_V}${RST}" + printf "%${padding}s" "" + printf "${BOLD}${color} %s ${RST}" "$title" + printf "%$(( width - 4 - title_len - padding ))s" "" + printf "${color}${BOX_V}${RST}\n" + + # Separator + printf "${color}${BOX_LT}" + printf "%0.s${BOX_H}" $(seq 1 $((width - 2))) + printf "${BOX_RT}${RST}\n" + fi +} + +# Close box +close_box() { + local width="${1:-40}" + local color="${2:-$BR_CYAN}" + printf "${color}${BOX_BL}" + printf "%0.s${BOX_H}" $(seq 1 $((width - 2))) + printf "${BOX_BR}${RST}\n" +} + +# Draw card (modern style) +draw_card() { + local title="$1" + local content="$2" + local width="${3:-50}" + local accent="${4:-$BR_CYAN}" + + # Top with accent bar + printf "${accent}▀${RST}" + printf "%0.s▀" $(seq 1 $((width - 2))) + printf "${accent}▀${RST}\n" + + # Content area + printf "${TEXT_MUTED}│${RST} ${BOLD}${accent}%s${RST}" "$title" + printf "%$(( width - ${#title} - 4 ))s" "" + printf "${TEXT_MUTED}│${RST}\n" + + if [[ -n "$content" ]]; then + printf "${TEXT_MUTED}│${RST} ${TEXT_SECONDARY}%s${RST}" "$content" + printf "%$(( width - ${#content} - 4 ))s" "" + printf "${TEXT_MUTED}│${RST}\n" + fi + + # Bottom + printf "${TEXT_MUTED}└" + printf "%0.s─" $(seq 1 $((width - 2))) + printf "┘${RST}\n" +} + +# Progress bar +progress_bar() { + local current="$1" + local total="$2" + local width="${3:-30}" + local color="${4:-$BR_GREEN}" + + local percentage=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "${color}" + printf "%0.s${PROG_FULL}" $(seq 1 "$filled" 2>/dev/null) || true + printf "${TEXT_MUTED}" + printf "%0.s${PROG_EMPTY}" $(seq 1 "$empty" 2>/dev/null) || true + printf "${RST} ${TEXT_SECONDARY}%3d%%${RST}" "$percentage" +} + +# Sparkline from array of values +sparkline() { + local -a values=("$@") + local max=1 + local min=0 + + # Find min/max + for v in "${values[@]}"; do + ((v > max)) && max=$v + ((v < min)) && min=$v + done + + local range=$((max - min)) + [[ $range -eq 0 ]] && range=1 + + local spark="" + for v in "${values[@]}"; do + local idx=$(( (v - min) * 7 / range )) + spark+="${SPARK_CHARS:$idx:1}" + done + + printf "%s" "$spark" +} + +# Status badge +badge() { + local text="$1" + local type="${2:-info}" + + case "$type" in + success) printf "${BR_BG_GREEN}${BOLD} %s ${RST}" "$text" ;; + error) printf "${BR_BG_PINK}${BOLD} %s ${RST}" "$text" ;; + warning) printf "\033[48;2;255;193;7m${BOLD}\033[38;2;0;0;0m %s ${RST}" "$text" ;; + info) printf "${BR_BG_CYAN}${BOLD}\033[38;2;0;0;0m %s ${RST}" "$text" ;; + *) printf "${BR_BG_PURPLE}${BOLD} %s ${RST}" "$text" ;; + esac +} + +# Status indicator with icon +status_indicator() { + local status="$1" + local label="$2" + + case "$status" in + online|up|ok|success) + printf "${COLOR_SUCCESS}${SYM_ONLINE} %s${RST}" "$label" + ;; + offline|down|error|fail) + printf "${COLOR_ERROR}${SYM_OFFLINE} %s${RST}" "$label" + ;; + warning|warn|degraded) + printf "${COLOR_WARNING}${SYM_WARNING} %s${RST}" "$label" + ;; + *) + printf "${COLOR_INFO}${SYM_INFO} %s${RST}" "$label" + ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOGGING SYSTEM +#─────────────────────────────────────────────────────────────────────────────── + +# Log levels +LOG_LEVEL="${LOG_LEVEL:-INFO}" +declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4) + +# Get current timestamp +timestamp() { + date "+%Y-%m-%d %H:%M:%S" +} + +# Log function +log() { + local level="${1:-INFO}" + shift + local message="$*" + + local level_num="${LOG_LEVELS[$level]:-1}" + local current_level_num="${LOG_LEVELS[$LOG_LEVEL]:-1}" + + [[ $level_num -lt $current_level_num ]] && return + + local color="" + case "$level" in + DEBUG) color="$TEXT_MUTED" ;; + INFO) color="$BR_CYAN" ;; + WARN) color="$BR_YELLOW" ;; + ERROR) color="$BR_RED" ;; + FATAL) color="$BR_RED$BOLD" ;; + esac + + # Log to file + local log_file="$BLACKROAD_LOGS/blackroad-$(date +%Y%m%d).log" + printf "[%s] [%s] %s\n" "$(timestamp)" "$level" "$message" >> "$log_file" + + # Output to terminal if not silent + [[ "${SILENT:-0}" != "1" ]] && printf "${color}[%s]${RST} %s\n" "$level" "$message" +} + +log_debug() { log DEBUG "$@"; } +log_info() { log INFO "$@"; } +log_warn() { log WARN "$@"; } +log_error() { log ERROR "$@"; } +log_fatal() { log FATAL "$@"; exit 1; } + +#─────────────────────────────────────────────────────────────────────────────── +# ERROR HANDLING & RETRY LOGIC +#─────────────────────────────────────────────────────────────────────────────── + +# Retry with exponential backoff +retry_with_backoff() { + local max_attempts="${1:-3}" + local base_delay="${2:-1}" + shift 2 + local cmd=("$@") + + local attempt=1 + local delay="$base_delay" + + while [[ $attempt -le $max_attempts ]]; do + log_debug "Attempt $attempt/$max_attempts: ${cmd[*]}" + + if "${cmd[@]}"; then + return 0 + fi + + if [[ $attempt -lt $max_attempts ]]; then + log_warn "Attempt $attempt failed, retrying in ${delay}s..." + sleep "$delay" + delay=$((delay * 2)) + fi + + ((attempt++)) + done + + log_error "All $max_attempts attempts failed for: ${cmd[*]}" + return 1 +} + +# Safe command execution with timeout +safe_exec() { + local timeout="${1:-10}" + shift + local cmd=("$@") + + if command -v timeout &>/dev/null; then + timeout "$timeout" "${cmd[@]}" + else + "${cmd[@]}" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# NETWORK UTILITIES +#─────────────────────────────────────────────────────────────────────────────── + +# Check if host is reachable +check_host() { + local host="$1" + local timeout="${2:-2}" + + ping -c 1 -W "$timeout" "$host" &>/dev/null +} + +# Check HTTP endpoint +check_http() { + local url="$1" + local timeout="${2:-5}" + local expected_code="${3:-200}" + + local code + code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout "$timeout" "$url" 2>/dev/null) + + [[ "$code" == "$expected_code" ]] +} + +# Get public IP +get_public_ip() { + curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || \ + curl -s --connect-timeout 5 https://ifconfig.me 2>/dev/null || \ + echo "unknown" +} + +# DNS lookup +dns_lookup() { + local domain="$1" + local type="${2:-A}" + + if command -v dig &>/dev/null; then + dig +short "$type" "$domain" 2>/dev/null | head -1 + elif command -v nslookup &>/dev/null; then + nslookup -type="$type" "$domain" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1 + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# DATE/TIME UTILITIES +#─────────────────────────────────────────────────────────────────────────────── + +# Human-readable time difference +time_ago() { + local seconds="$1" + + if [[ $seconds -lt 60 ]]; then + echo "${seconds}s ago" + elif [[ $seconds -lt 3600 ]]; then + echo "$((seconds / 60))m ago" + elif [[ $seconds -lt 86400 ]]; then + echo "$((seconds / 3600))h ago" + else + echo "$((seconds / 86400))d ago" + fi +} + +# Parse ISO date to epoch +iso_to_epoch() { + local iso_date="$1" + date -d "$iso_date" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$iso_date" +%s 2>/dev/null +} + +# Format bytes to human readable +format_bytes() { + local bytes="$1" + local units=("B" "KB" "MB" "GB" "TB") + local unit=0 + + while [[ $bytes -ge 1024 && $unit -lt 4 ]]; do + bytes=$((bytes / 1024)) + ((unit++)) + done + + printf "%d%s" "$bytes" "${units[$unit]}" +} + +# Format number with commas +format_number() { + local num="$1" + printf "%'d" "$num" 2>/dev/null || echo "$num" +} + +#─────────────────────────────────────────────────────────────────────────────── +# SYSTEM INFORMATION +#─────────────────────────────────────────────────────────────────────────────── + +# Get OS type +get_os() { + case "$(uname -s)" in + Darwin*) echo "macos" ;; + Linux*) echo "linux" ;; + CYGWIN*|MINGW*|MSYS*) echo "windows" ;; + *) echo "unknown" ;; + esac +} + +# Get CPU usage +get_cpu_usage() { + local os=$(get_os) + case "$os" in + macos) + top -l 1 | grep "CPU usage" | awk '{print int($3)}' 2>/dev/null + ;; + linux) + grep 'cpu ' /proc/stat | awk '{usage=($2+$4)*100/($2+$4+$5)} END {print int(usage)}' 2>/dev/null + ;; + *) + echo "0" + ;; + esac +} + +# Get memory usage +get_memory_usage() { + local os=$(get_os) + case "$os" in + macos) + vm_stat | awk '/Pages active/ {active=$3} /Pages wired/ {wired=$4} /Pages free/ {free=$3} END {used=active+wired; total=used+free; print int(used*100/total)}' 2>/dev/null + ;; + linux) + free | awk '/Mem:/ {print int($3*100/$2)}' 2>/dev/null + ;; + *) + echo "0" + ;; + esac +} + +# Get disk usage +get_disk_usage() { + local path="${1:-/}" + df -h "$path" 2>/dev/null | awk 'NR==2 {gsub(/%/,"",$5); print $5}' +} + +# Get uptime in seconds +get_uptime_seconds() { + local os=$(get_os) + case "$os" in + macos) + sysctl -n kern.boottime 2>/dev/null | awk '{print int(systime() - $4)}' | tr -d ',' + ;; + linux) + cat /proc/uptime 2>/dev/null | awk '{print int($1)}' + ;; + *) + echo "0" + ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# ANIMATION HELPERS +#─────────────────────────────────────────────────────────────────────────────── + +# Spinner animation +spinner() { + local pid="$1" + local message="${2:-Loading}" + local spin_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + local i=0 + + cursor_hide + while kill -0 "$pid" 2>/dev/null; do + printf "\r${BR_CYAN}${spin_chars:$i:1}${RST} %s..." "$message" + i=$(( (i + 1) % 10 )) + sleep 0.1 + done + printf "\r%${TERM_COLS}s\r" + cursor_show +} + +# Typing effect +type_text() { + local text="$1" + local delay="${2:-0.03}" + + for ((i=0; i<${#text}; i++)); do + printf "%s" "${text:$i:1}" + sleep "$delay" + done +} + +# Fade in text +fade_in() { + local text="$1" + local steps=5 + + for ((i=1; i<=steps; i++)); do + local brightness=$((50 + i * 40)) + printf "\r\033[38;2;%d;%d;%dm%s${RST}" "$brightness" "$brightness" "$brightness" "$text" + sleep 0.05 + done + printf "\n" +} + +# Pulse animation (single frame) +pulse_frame() { + local text="$1" + local frame="$2" + local colors=( + "255;100;100" + "255;150;100" + "255;200;100" + "255;255;100" + "255;200;100" + "255;150;100" + ) + local color="${colors[$((frame % ${#colors[@]}))]}" + printf "\033[38;2;%sm%s${RST}" "$color" "$text" +} + +#─────────────────────────────────────────────────────────────────────────────── +# KEYBOARD INPUT +#─────────────────────────────────────────────────────────────────────────────── + +# Read single keypress (non-blocking) +read_key() { + local timeout="${1:-0.1}" + local key="" + + if read -rsn1 -t "$timeout" key 2>/dev/null; then + # Handle escape sequences + if [[ "$key" == $'\e' ]]; then + read -rsn2 -t 0.01 key2 2>/dev/null + key+="$key2" + fi + echo "$key" + return 0 + fi + return 1 +} + +# Map key to action +key_to_action() { + local key="$1" + case "$key" in + q|Q) echo "quit" ;; + r|R) echo "refresh" ;; + h|H|\?) echo "help" ;; + s|S) echo "ssh" ;; + t|T) echo "theme" ;; + e|E) echo "export" ;; + $'\e[A') echo "up" ;; + $'\e[B') echo "down" ;; + $'\e[C') echo "right" ;; + $'\e[D') echo "left" ;; + $'\e') echo "escape" ;; + ' ') echo "select" ;; + $'\n') echo "enter" ;; + *) echo "unknown" ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# MENU SYSTEM +#─────────────────────────────────────────────────────────────────────────────── + +# Draw menu +draw_menu() { + local title="$1" + shift + local -a items=("$@") + local selected="${MENU_SELECTED:-0}" + local width=50 + + draw_box "$title" "$width" "$BR_PURPLE" + + for i in "${!items[@]}"; do + if [[ $i -eq $selected ]]; then + printf "${BR_PURPLE}${BOX_V}${RST} ${BR_BG_PURPLE}${BOLD} → ${items[$i]}${RST}" + else + printf "${BR_PURPLE}${BOX_V}${RST} ${TEXT_SECONDARY}${items[$i]}${RST}" + fi + local item_len=$((${#items[$i]} + 5)) + printf "%$(( width - item_len - 3 ))s" + printf "${BR_PURPLE}${BOX_V}${RST}\n" + done + + close_box "$width" "$BR_PURPLE" +} + +#─────────────────────────────────────────────────────────────────────────────── +# NOTIFICATION HELPERS (used by notification-system.sh) +#─────────────────────────────────────────────────────────────────────────────── + +# Play sound if available +play_notification_sound() { + local sound_type="${1:-default}" + + if [[ "$(get_os)" == "macos" ]]; then + case "$sound_type" in + success) afplay /System/Library/Sounds/Glass.aiff 2>/dev/null & ;; + error) afplay /System/Library/Sounds/Basso.aiff 2>/dev/null & ;; + warning) afplay /System/Library/Sounds/Sosumi.aiff 2>/dev/null & ;; + *) afplay /System/Library/Sounds/Pop.aiff 2>/dev/null & ;; + esac + elif command -v paplay &>/dev/null; then + paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null & + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# CLEANUP HANDLER +#─────────────────────────────────────────────────────────────────────────────── + +cleanup() { + cursor_show + stty echo 2>/dev/null + rm -rf "$BLACKROAD_TEMP" 2>/dev/null + printf "${RST}\n" +} + +# Set trap for cleanup +trap cleanup EXIT INT TERM + +#─────────────────────────────────────────────────────────────────────────────── +# INITIALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +# Initialize on source +get_term_size + +# Export functions for subshells +export -f log log_debug log_info log_warn log_error +export -f check_host check_http get_public_ip +export -f progress_bar sparkline badge status_indicator +export -f cursor_to clear_screen cursor_hide cursor_show +export -f timestamp time_ago format_bytes format_number +export -f get_os get_cpu_usage get_memory_usage get_disk_usage + +log_debug "BlackRoad Core Library v2.0 loaded" diff --git a/lib-parallel.sh b/lib-parallel.sh new file mode 100644 index 0000000..6059de9 --- /dev/null +++ b/lib-parallel.sh @@ -0,0 +1,485 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD PARALLEL EXECUTION LIBRARY v2.0 +# High-performance concurrent task execution with job management +#═══════════════════════════════════════════════════════════════════════════════ + +# Source core library if not already loaded +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -z "$BLACKROAD_CORE_LOADED" ]] && source "$SCRIPT_DIR/lib-core.sh" + +# Prevent multiple inclusions +[[ -n "$BLACKROAD_PARALLEL_LOADED" ]] && return 0 +export BLACKROAD_PARALLEL_LOADED=1 + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +export PARALLEL_MAX_JOBS="${PARALLEL_MAX_JOBS:-8}" +export PARALLEL_TEMP_DIR="${BLACKROAD_TEMP:-/tmp/blackroad-$$}/parallel" +export PARALLEL_TIMEOUT="${PARALLEL_TIMEOUT:-30}" + +# Ensure temp directory exists +mkdir -p "$PARALLEL_TEMP_DIR" 2>/dev/null + +# Job tracking +declare -A PARALLEL_JOBS +declare -A PARALLEL_RESULTS +declare -A PARALLEL_STATUS +PARALLEL_JOB_COUNT=0 + +#─────────────────────────────────────────────────────────────────────────────── +# JOB MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +# Generate unique job ID +parallel_job_id() { + echo "job_$$_$((++PARALLEL_JOB_COUNT))_$(date +%s%N | tail -c 6)" +} + +# Submit a job for parallel execution +parallel_submit() { + local job_name="$1" + shift + local cmd=("$@") + + local job_id=$(parallel_job_id) + local output_file="$PARALLEL_TEMP_DIR/${job_id}.out" + local status_file="$PARALLEL_TEMP_DIR/${job_id}.status" + local time_file="$PARALLEL_TEMP_DIR/${job_id}.time" + + # Wait if at max capacity + while [[ $(jobs -r | wc -l) -ge $PARALLEL_MAX_JOBS ]]; do + sleep 0.1 + done + + # Execute in background with timing + ( + local start_time=$(date +%s%N) + "${cmd[@]}" > "$output_file" 2>&1 + local exit_code=$? + local end_time=$(date +%s%N) + local duration=$(( (end_time - start_time) / 1000000 )) # milliseconds + + echo "$exit_code" > "$status_file" + echo "$duration" > "$time_file" + ) & + + local pid=$! + PARALLEL_JOBS[$job_id]="$pid" + PARALLEL_STATUS[$job_id]="running" + + log_debug "Parallel job submitted: $job_id ($job_name) - PID: $pid" + echo "$job_id" +} + +# Wait for a specific job to complete +parallel_wait_job() { + local job_id="$1" + local timeout="${2:-$PARALLEL_TIMEOUT}" + + local pid="${PARALLEL_JOBS[$job_id]}" + [[ -z "$pid" ]] && return 1 + + local start_time=$(date +%s) + + while kill -0 "$pid" 2>/dev/null; do + local now=$(date +%s) + if [[ $((now - start_time)) -gt $timeout ]]; then + log_warn "Job $job_id timed out, killing..." + kill -9 "$pid" 2>/dev/null + PARALLEL_STATUS[$job_id]="timeout" + return 124 + fi + sleep 0.1 + done + + wait "$pid" 2>/dev/null + PARALLEL_STATUS[$job_id]="completed" + + return 0 +} + +# Wait for all jobs to complete +parallel_wait_all() { + local timeout="${1:-$PARALLEL_TIMEOUT}" + + for job_id in "${!PARALLEL_JOBS[@]}"; do + parallel_wait_job "$job_id" "$timeout" + done +} + +# Get job result +parallel_get_result() { + local job_id="$1" + local output_file="$PARALLEL_TEMP_DIR/${job_id}.out" + + [[ -f "$output_file" ]] && cat "$output_file" +} + +# Get job status +parallel_get_status() { + local job_id="$1" + local status_file="$PARALLEL_TEMP_DIR/${job_id}.status" + + if [[ -f "$status_file" ]]; then + cat "$status_file" + else + echo "-1" + fi +} + +# Get job execution time (ms) +parallel_get_time() { + local job_id="$1" + local time_file="$PARALLEL_TEMP_DIR/${job_id}.time" + + if [[ -f "$time_file" ]]; then + cat "$time_file" + else + echo "0" + fi +} + +# Check if job is complete +parallel_is_complete() { + local job_id="$1" + local status_file="$PARALLEL_TEMP_DIR/${job_id}.status" + + [[ -f "$status_file" ]] +} + +#─────────────────────────────────────────────────────────────────────────────── +# BATCH OPERATIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Run multiple commands in parallel, return results as array +parallel_batch() { + local -a commands=("$@") + local -a job_ids=() + local -a results=() + + # Submit all jobs + for cmd in "${commands[@]}"; do + local job_id=$(parallel_submit "batch" bash -c "$cmd") + job_ids+=("$job_id") + done + + # Wait for all to complete + for job_id in "${job_ids[@]}"; do + parallel_wait_job "$job_id" + results+=("$(parallel_get_result "$job_id")") + done + + # Return results (newline separated) + printf '%s\n' "${results[@]}" +} + +# Run function on each item in array (parallel map) +parallel_map() { + local func="$1" + shift + local -a items=("$@") + local -a job_ids=() + + # Submit jobs + for item in "${items[@]}"; do + local job_id=$(parallel_submit "map_$func" "$func" "$item") + job_ids+=("$job_id") + done + + # Collect results + for job_id in "${job_ids[@]}"; do + parallel_wait_job "$job_id" + parallel_get_result "$job_id" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# API-SPECIFIC PARALLEL FETCHERS +#─────────────────────────────────────────────────────────────────────────────── + +# Fetch multiple URLs in parallel +parallel_fetch_urls() { + local -a urls=("$@") + local -a job_ids=() + local -a results=() + + # Submit all fetch jobs + for url in "${urls[@]}"; do + local job_id=$(parallel_submit "fetch" curl -s --connect-timeout 10 "$url") + job_ids+=("$job_id") + done + + # Wait and collect results + for i in "${!job_ids[@]}"; do + parallel_wait_job "${job_ids[$i]}" + local result=$(parallel_get_result "${job_ids[$i]}") + local status=$(parallel_get_status "${job_ids[$i]}") + local time=$(parallel_get_time "${job_ids[$i]}") + + # Output as JSON object + printf '{"url":"%s","status":%s,"time_ms":%s,"data":%s}\n' \ + "${urls[$i]}" "$status" "$time" "${result:-null}" + done +} + +# Ping multiple hosts in parallel +parallel_ping_hosts() { + local -a hosts=("$@") + local -a job_ids=() + + # Submit ping jobs + for host in "${hosts[@]}"; do + local job_id=$(parallel_submit "ping_$host" ping -c 1 -W 2 "$host") + job_ids+=("$job_id") + done + + # Collect results + for i in "${!job_ids[@]}"; do + parallel_wait_job "${job_ids[$i]}" + local status=$(parallel_get_status "${job_ids[$i]}") + local time=$(parallel_get_time "${job_ids[$i]}") + + if [[ "$status" == "0" ]]; then + printf '{"host":"%s","status":"online","latency_ms":%s}\n' "${hosts[$i]}" "$time" + else + printf '{"host":"%s","status":"offline","latency_ms":null}\n' "${hosts[$i]}" + fi + done +} + +# Check multiple HTTP endpoints +parallel_http_check() { + local -a endpoints=("$@") + local -a job_ids=() + + for endpoint in "${endpoints[@]}"; do + local job_id=$(parallel_submit "http_$endpoint" \ + curl -s -o /dev/null -w "%{http_code}|%{time_total}" --connect-timeout 5 "$endpoint") + job_ids+=("$job_id") + done + + for i in "${!job_ids[@]}"; do + parallel_wait_job "${job_ids[$i]}" + local result=$(parallel_get_result "${job_ids[$i]}") + local code=$(echo "$result" | cut -d'|' -f1) + local time=$(echo "$result" | cut -d'|' -f2) + + local status="up" + [[ "$code" != "200" ]] && [[ "$code" != "201" ]] && [[ "$code" != "204" ]] && status="down" + + printf '{"url":"%s","http_code":%s,"response_time":"%s","status":"%s"}\n' \ + "${endpoints[$i]}" "${code:-0}" "${time:-0}" "$status" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# GITHUB API PARALLEL +#─────────────────────────────────────────────────────────────────────────────── + +# Fetch multiple GitHub repos in parallel +parallel_github_repos() { + local -a repos=("$@") + local -a job_ids=() + local auth_header="" + + [[ -n "$GITHUB_TOKEN" ]] && auth_header="-H \"Authorization: token $GITHUB_TOKEN\"" + + for repo in "${repos[@]}"; do + local job_id=$(parallel_submit "gh_$repo" bash -c \ + "curl -s $auth_header 'https://api.github.com/repos/$repo'") + job_ids+=("$job_id") + done + + for job_id in "${job_ids[@]}"; do + parallel_wait_job "$job_id" + parallel_get_result "$job_id" + done +} + +# Fetch GitHub user data in parallel +parallel_github_users() { + local -a users=("$@") + local -a job_ids=() + + for user in "${users[@]}"; do + local job_id=$(parallel_submit "gh_user_$user" \ + curl -s "https://api.github.com/users/$user") + job_ids+=("$job_id") + done + + for job_id in "${job_ids[@]}"; do + parallel_wait_job "$job_id" + parallel_get_result "$job_id" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# CLOUDFLARE API PARALLEL +#─────────────────────────────────────────────────────────────────────────────── + +# Fetch multiple Cloudflare zones in parallel +parallel_cloudflare_zones() { + local -a zone_ids=("$@") + local -a job_ids=() + + [[ -z "$CLOUDFLARE_TOKEN" ]] && { log_error "CLOUDFLARE_TOKEN not set"; return 1; } + + for zone_id in "${zone_ids[@]}"; do + local job_id=$(parallel_submit "cf_$zone_id" bash -c \ + "curl -s -H 'Authorization: Bearer $CLOUDFLARE_TOKEN' \ + -H 'Content-Type: application/json' \ + 'https://api.cloudflare.com/client/v4/zones/$zone_id'") + job_ids+=("$job_id") + done + + for job_id in "${job_ids[@]}"; do + parallel_wait_job "$job_id" + parallel_get_result "$job_id" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# DASHBOARD DATA PARALLEL FETCH +#─────────────────────────────────────────────────────────────────────────────── + +# Fetch all dashboard data in parallel (composite function) +parallel_fetch_dashboard_data() { + local -a results=() + + # Submit all data fetches at once + local github_job=$(parallel_submit "github" \ + curl -s "https://api.github.com/users/blackboxprogramming/repos?per_page=100") + + local crypto_btc=$(parallel_submit "btc" \ + curl -s "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true") + + local crypto_eth=$(parallel_submit "eth" \ + curl -s "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd&include_24hr_change=true") + + local crypto_sol=$(parallel_submit "sol" \ + curl -s "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true") + + local iss_job=$(parallel_submit "iss" \ + curl -s "http://api.open-notify.org/iss-now.json") + + local earthquakes_job=$(parallel_submit "earthquakes" \ + curl -s "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_hour.geojson") + + # Wait for all + parallel_wait_all + + # Output combined results + cat << EOF +{ + "github": $(parallel_get_result "$github_job"), + "crypto": { + "bitcoin": $(parallel_get_result "$crypto_btc"), + "ethereum": $(parallel_get_result "$crypto_eth"), + "solana": $(parallel_get_result "$crypto_sol") + }, + "iss": $(parallel_get_result "$iss_job"), + "earthquakes": $(parallel_get_result "$earthquakes_job"), + "fetched_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "fetch_stats": { + "github_ms": $(parallel_get_time "$github_job"), + "crypto_btc_ms": $(parallel_get_time "$crypto_btc"), + "crypto_eth_ms": $(parallel_get_time "$crypto_eth"), + "crypto_sol_ms": $(parallel_get_time "$crypto_sol"), + "iss_ms": $(parallel_get_time "$iss_job"), + "earthquakes_ms": $(parallel_get_time "$earthquakes_job") + } +} +EOF +} + +#─────────────────────────────────────────────────────────────────────────────── +# PROGRESS TRACKING +#─────────────────────────────────────────────────────────────────────────────── + +# Show parallel execution progress +parallel_show_progress() { + local total="${1:-${#PARALLEL_JOBS[@]}}" + local completed=0 + local running=0 + + for job_id in "${!PARALLEL_JOBS[@]}"; do + if parallel_is_complete "$job_id"; then + ((completed++)) + else + ((running++)) + fi + done + + # Draw progress bar + printf "\r${BR_CYAN}Jobs:${RST} " + progress_bar "$completed" "$total" 20 "$BR_GREEN" + printf " ${TEXT_SECONDARY}(%d/%d complete, %d running)${RST}" "$completed" "$total" "$running" +} + +# Animated progress while waiting +parallel_progress_wait() { + local refresh_interval="${1:-0.2}" + local total=${#PARALLEL_JOBS[@]} + local spin_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' + local spin_idx=0 + + cursor_hide + + while true; do + local completed=0 + local running=0 + + for job_id in "${!PARALLEL_JOBS[@]}"; do + if parallel_is_complete "$job_id"; then + ((completed++)) + else + ((running++)) + fi + done + + # Draw progress + printf "\r${BR_CYAN}${spin_chars:$spin_idx:1}${RST} Fetching data... " + progress_bar "$completed" "$total" 25 "$BR_GREEN" + printf " ${TEXT_MUTED}[%d/%d]${RST}" "$completed" "$total" + + [[ $completed -eq $total ]] && break + + spin_idx=$(( (spin_idx + 1) % 10 )) + sleep "$refresh_interval" + done + + cursor_show + printf "\r%${TERM_COLS}s\r" # Clear line +} + +#─────────────────────────────────────────────────────────────────────────────── +# CLEANUP +#─────────────────────────────────────────────────────────────────────────────── + +# Clean up parallel execution resources +parallel_cleanup() { + # Kill any remaining jobs + for job_id in "${!PARALLEL_JOBS[@]}"; do + local pid="${PARALLEL_JOBS[$job_id]}" + kill -9 "$pid" 2>/dev/null + done + + # Clear temp files + rm -rf "$PARALLEL_TEMP_DIR" 2>/dev/null + + # Reset state + PARALLEL_JOBS=() + PARALLEL_RESULTS=() + PARALLEL_STATUS=() + PARALLEL_JOB_COUNT=0 + + log_debug "Parallel execution cleaned up" +} + +# Add to cleanup trap +trap parallel_cleanup EXIT INT TERM + +log_debug "BlackRoad Parallel Library v2.0 loaded" diff --git a/log-streaming.sh b/log-streaming.sh new file mode 100644 index 0000000..66f237a --- /dev/null +++ b/log-streaming.sh @@ -0,0 +1,516 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██████╗ ██████╗ ███████╗ +# ██║ ██╔═══██╗██╔════╝ ██╔════╝ +# ██║ ██║ ██║██║ ███╗███████╗ +# ██║ ██║ ██║██║ ██║╚════██║ +# ███████╗╚██████╔╝╚██████╔╝███████║ +# ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD LOG STREAMING DASHBOARD v2.0 +# Real-time log aggregation, filtering, and visualization +#═══════════════════════════════════════════════════════════════════════════════ + +# Source libraries +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +LOG_BUFFER_SIZE=1000 +LOG_DISPLAY_LINES=20 +LOG_UPDATE_INTERVAL=1 + +# Log sources +declare -A LOG_SOURCES=( + ["blackroad"]="$HOME/.blackroad-dashboards/logs/blackroad-*.log" + ["system"]="/var/log/syslog:/var/log/system.log" + ["nginx"]="/var/log/nginx/access.log:/var/log/nginx/error.log" + ["docker"]="docker logs" + ["journal"]="journalctl -f" +) + +# Log level colors +declare -A LOG_LEVEL_COLORS=( + ["DEBUG"]="$TEXT_MUTED" + ["INFO"]="$BR_CYAN" + ["WARN"]="$BR_YELLOW" + ["WARNING"]="$BR_YELLOW" + ["ERROR"]="$BR_RED" + ["FATAL"]="$BR_RED$BOLD" + ["CRITICAL"]="$BR_RED$BOLD$BLINK" +) + +# Log buffer +declare -a LOG_BUFFER=() +LOG_PAUSED=0 +LOG_FILTER="" +LOG_LEVEL_FILTER="" +LOG_SOURCE_FILTER="" + +#─────────────────────────────────────────────────────────────────────────────── +# LOG PARSING +#─────────────────────────────────────────────────────────────────────────────── + +# Parse log line and extract components +parse_log_line() { + local line="$1" + + local timestamp="" + local level="INFO" + local source="" + local message="" + + # Try to extract timestamp [YYYY-MM-DD HH:MM:SS] + if [[ "$line" =~ \[([0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2})\] ]]; then + timestamp="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}) ]]; then + timestamp="${BASH_REMATCH[1]}" + fi + + # Extract log level + if [[ "$line" =~ \[(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|CRITICAL)\] ]]; then + level="${BASH_REMATCH[1]}" + elif [[ "$line" =~ (debug|info|warn|warning|error|fatal|critical) ]]; then + level=$(echo "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]') + fi + + # Extract source if present + if [[ "$line" =~ \[([a-zA-Z0-9_-]+)\]: ]]; then + source="${BASH_REMATCH[1]}" + fi + + # Rest is message + message="$line" + + echo "$timestamp|$level|$source|$message" +} + +# Colorize log line based on level +colorize_log() { + local line="$1" + local level="INFO" + + # Detect level + for lvl in "${!LOG_LEVEL_COLORS[@]}"; do + if [[ "$line" =~ \[$lvl\] ]] || [[ "$line" =~ $lvl ]]; then + level="$lvl" + break + fi + done + + local color="${LOG_LEVEL_COLORS[$level]:-$TEXT_SECONDARY}" + + # Highlight specific patterns + local highlighted="$line" + + # Highlight timestamps + highlighted=$(echo "$highlighted" | sed -E "s/(\[[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]][0-9]{2}:[0-9]{2}:[0-9]{2}\])/${TEXT_MUTED}\1${color}/g") + + # Highlight log levels + highlighted=$(echo "$highlighted" | sed -E "s/\[(DEBUG|INFO|WARN|WARNING|ERROR|FATAL|CRITICAL)\]/${BOLD}[\1]${RST}${color}/g") + + # Highlight IPs + highlighted=$(echo "$highlighted" | sed -E "s/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/${BR_PURPLE}\1${color}/g") + + # Highlight URLs + highlighted=$(echo "$highlighted" | sed -E "s/(https?:\/\/[^[:space:]]+)/${BR_CYAN}\1${color}/g") + + printf "${color}%s${RST}\n" "$highlighted" +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOG BUFFER MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +# Add line to buffer +add_to_buffer() { + local line="$1" + local source="${2:-unknown}" + + # Skip empty lines + [[ -z "$line" ]] && return + + # Add to buffer with source tag + LOG_BUFFER+=("[$(date '+%H:%M:%S')] [$source] $line") + + # Trim buffer if too large + if [[ ${#LOG_BUFFER[@]} -gt $LOG_BUFFER_SIZE ]]; then + LOG_BUFFER=("${LOG_BUFFER[@]:$((${#LOG_BUFFER[@]} - LOG_BUFFER_SIZE))}") + fi +} + +# Filter buffer +filter_buffer() { + local -a filtered=() + + for line in "${LOG_BUFFER[@]}"; do + local include=true + + # Text filter + if [[ -n "$LOG_FILTER" ]]; then + if ! [[ "$line" =~ $LOG_FILTER ]]; then + include=false + fi + fi + + # Level filter + if [[ -n "$LOG_LEVEL_FILTER" ]]; then + if ! [[ "$line" =~ \[$LOG_LEVEL_FILTER\] ]]; then + include=false + fi + fi + + # Source filter + if [[ -n "$LOG_SOURCE_FILTER" ]]; then + if ! [[ "$line" =~ \[$LOG_SOURCE_FILTER\] ]]; then + include=false + fi + fi + + [[ "$include" == "true" ]] && filtered+=("$line") + done + + printf '%s\n' "${filtered[@]}" +} + +# Clear buffer +clear_buffer() { + LOG_BUFFER=() +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOG SOURCES +#─────────────────────────────────────────────────────────────────────────────── + +# Start tailing a file +tail_file() { + local file="$1" + local source_name="${2:-$(basename "$file")}" + + if [[ -f "$file" ]]; then + tail -F "$file" 2>/dev/null | while IFS= read -r line; do + [[ "$LOG_PAUSED" == "0" ]] && add_to_buffer "$line" "$source_name" + done & + echo $! + fi +} + +# Start tailing a command +tail_command() { + local cmd="$1" + local source_name="${2:-cmd}" + + eval "$cmd" 2>/dev/null | while IFS= read -r line; do + [[ "$LOG_PAUSED" == "0" ]] && add_to_buffer "$line" "$source_name" + done & + echo $! +} + +# Start all configured sources +start_all_sources() { + local -a pids=() + + for source_name in "${!LOG_SOURCES[@]}"; do + local config="${LOG_SOURCES[$source_name]}" + + if [[ "$config" =~ ^docker|^journalctl ]]; then + # Command-based source + local pid=$(tail_command "$config" "$source_name") + [[ -n "$pid" ]] && pids+=("$pid") + else + # File-based source(s) + IFS=':' read -ra files <<< "$config" + for file_pattern in "${files[@]}"; do + for file in $file_pattern; do + [[ -f "$file" ]] && { + local pid=$(tail_file "$file" "$source_name") + [[ -n "$pid" ]] && pids+=("$pid") + } + done + done + fi + done + + echo "${pids[*]}" +} + +# Stop all sources +stop_all_sources() { + local pids="$1" + + for pid in $pids; do + kill "$pid" 2>/dev/null + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOG DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_log_header() { + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 📜 BLACKROAD LOG STREAM ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}" + + # Status bar + local pause_status="${BR_GREEN}LIVE${RST}" + [[ "$LOG_PAUSED" == "1" ]] && pause_status="${BR_YELLOW}PAUSED${RST}" + + printf " Status: $pause_status" + printf " │ Lines: ${TEXT_SECONDARY}${#LOG_BUFFER[@]}${RST}" + [[ -n "$LOG_FILTER" ]] && printf " │ Filter: ${BR_PURPLE}$LOG_FILTER${RST}" + [[ -n "$LOG_LEVEL_FILTER" ]] && printf " │ Level: ${BR_ORANGE}$LOG_LEVEL_FILTER${RST}" + printf "\n" + printf "${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" +} + +render_log_lines() { + local lines_to_show="$LOG_DISPLAY_LINES" + local filtered_lines + mapfile -t filtered_lines < <(filter_buffer | tail -n "$lines_to_show") + + for line in "${filtered_lines[@]}"; do + colorize_log "$line" + done + + # Pad remaining lines + local shown=${#filtered_lines[@]} + for ((i=shown; i/dev/null + get_term_size + + # Adjust display lines based on terminal size + LOG_DISPLAY_LINES=$((TERM_ROWS - 8)) + + # Start log sources + local pids=$(start_all_sources) + + # Add some initial demo logs + add_to_buffer "[INFO] Log streaming dashboard started" "system" + add_to_buffer "[INFO] Monitoring ${#LOG_SOURCES[@]} log sources" "system" + + local last_render=0 + + while true; do + local now=$(date +%s) + + # Render at interval + if [[ $((now - last_render)) -ge $LOG_UPDATE_INTERVAL ]]; then + render_log_dashboard + last_render=$now + fi + + # Handle input + if read -rsn1 -t 0.5 key 2>/dev/null; then + case "$key" in + q|Q) + stop_all_sources "$pids" + break + ;; + p|P) + LOG_PAUSED=$((1 - LOG_PAUSED)) + ;; + c|C) + clear_buffer + add_to_buffer "[INFO] Log buffer cleared" "system" + ;; + f|/) + prompt_filter + ;; + l|L) + prompt_level_filter + ;; + x|X) + LOG_FILTER="" + LOG_LEVEL_FILTER="" + LOG_SOURCE_FILTER="" + ;; + +) + LOG_DISPLAY_LINES=$((LOG_DISPLAY_LINES + 5)) + ;; + -) + [[ $LOG_DISPLAY_LINES -gt 5 ]] && LOG_DISPLAY_LINES=$((LOG_DISPLAY_LINES - 5)) + ;; + esac + fi + done + + cursor_show + stty echo 2>/dev/null + clear_screen +} + +#─────────────────────────────────────────────────────────────────────────────── +# LOG ANALYSIS +#─────────────────────────────────────────────────────────────────────────────── + +# Analyze log patterns +analyze_logs() { + local -A level_counts=() + local -A source_counts=() + local total=0 + + for line in "${LOG_BUFFER[@]}"; do + ((total++)) + + # Count levels + for level in "${!LOG_LEVEL_COLORS[@]}"; do + if [[ "$line" =~ \[$level\] ]]; then + level_counts[$level]=$((${level_counts[$level]:-0} + 1)) + break + fi + done + + # Count sources + if [[ "$line" =~ \[([a-zA-Z0-9_-]+)\] ]]; then + local source="${BASH_REMATCH[1]}" + source_counts[$source]=$((${source_counts[$source]:-0} + 1)) + fi + done + + printf "${BOLD}Log Analysis (${total} entries):${RST}\n\n" + + printf "${BR_CYAN}By Level:${RST}\n" + for level in DEBUG INFO WARN WARNING ERROR FATAL CRITICAL; do + local count="${level_counts[$level]:-0}" + [[ $count -gt 0 ]] && printf " %-10s %d\n" "$level" "$count" + done + + printf "\n${BR_PURPLE}By Source:${RST}\n" + for source in "${!source_counts[@]}"; do + printf " %-15s %d\n" "$source" "${source_counts[$source]}" + done +} + +# Search logs +search_logs() { + local pattern="$1" + local context="${2:-0}" + + local -a matches=() + local idx=0 + + for line in "${LOG_BUFFER[@]}"; do + if [[ "$line" =~ $pattern ]]; then + # Add context before + for ((i=idx-context; i "$output_file" + ;; + json) + printf '[\n' + local first=true + for line in "${LOG_BUFFER[@]}"; do + [[ "$first" != "true" ]] && printf ',\n' + first=false + printf ' "%s"' "$(echo "$line" | sed 's/"/\\"/g')" + done + printf '\n]\n' + ;; + esac > "$output_file" + + printf "Exported to: %s\n" "$output_file" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-stream}" in + stream) log_dashboard_loop ;; + analyze) analyze_logs ;; + search) search_logs "$2" "${3:-0}" ;; + export) export_logs "${2:-txt}" "$3" ;; + tail) + # Simple tail mode + tail_file "$2" "${3:-file}" & + wait + ;; + *) + printf "Usage: %s [stream|analyze|search|export|tail]\n" "$0" + printf " %s stream # Interactive log dashboard\n" "$0" + printf " %s search 'error' # Search logs\n" "$0" + printf " %s export json # Export logs\n" "$0" + printf " %s tail /var/log/syslog # Tail specific file\n" "$0" + ;; + esac +fi diff --git a/markdown-renderer.sh b/markdown-renderer.sh new file mode 100644 index 0000000..e226ab2 --- /dev/null +++ b/markdown-renderer.sh @@ -0,0 +1,486 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗███╗ ██╗ +# ████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝██╔══██╗██╔═══██╗██║ ██║████╗ ██║ +# ██╔████╔██║███████║██████╔╝█████╔╝ ██║ ██║██║ ██║██║ █╗ ██║██╔██╗ ██║ +# ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║██║ ██║██║███╗██║██║╚██╗██║ +# ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗██████╔╝╚██████╔╝╚███╔███╔╝██║ ╚████║ +# ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═══╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD MARKDOWN RENDERER v1.0 +# Terminal Markdown Viewer with Syntax Highlighting +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# COLOR DEFINITIONS +#─────────────────────────────────────────────────────────────────────────────── + +C_RESET="\033[0m" +C_BOLD="\033[1m" +C_DIM="\033[2m" +C_ITALIC="\033[3m" +C_UNDERLINE="\033[4m" +C_STRIKE="\033[9m" + +C_H1="\033[1;38;5;214m" +C_H2="\033[1;38;5;39m" +C_H3="\033[1;38;5;46m" +C_H4="\033[1;38;5;201m" +C_H5="\033[1;38;5;226m" +C_H6="\033[1;38;5;244m" + +C_CODE="\033[48;5;236m\033[38;5;208m" +C_CODE_BLOCK="\033[48;5;235m" +C_LINK="\033[38;5;39m" +C_LINK_URL="\033[38;5;244m" +C_QUOTE="\033[38;5;244m" +C_LIST="\033[38;5;46m" +C_HR="\033[38;5;240m" +C_TABLE="\033[38;5;240m" +C_IMAGE="\033[38;5;201m" + +#─────────────────────────────────────────────────────────────────────────────── +# SYNTAX HIGHLIGHTING FOR CODE BLOCKS +#─────────────────────────────────────────────────────────────────────────────── + +declare -A SYNTAX_KEYWORDS=( + [bash]="if then else elif fi case esac for while do done function return local export source" + [python]="def class if elif else for while try except finally with as import from return yield lambda" + [javascript]="function const let var if else for while switch case break continue return class import export async await" + [go]="func package import type struct interface if else for switch case return defer go chan" + [rust]="fn let mut if else for while loop match return struct enum impl trait use mod pub" +) + +declare -A SYNTAX_TYPES=( + [bash]="string int array" + [python]="str int float list dict tuple set bool None True False" + [javascript]="string number boolean null undefined Array Object" + [go]="int string bool byte rune error nil true false" + [rust]="i32 i64 u32 u64 f32 f64 bool String str Vec Option Result" +) + +highlight_code() { + local code="$1" + local lang="$2" + + # Keywords + local keywords="${SYNTAX_KEYWORDS[$lang]}" + for kw in $keywords; do + code=$(echo "$code" | sed "s/\b${kw}\b/\\\\033[38;5;201m${kw}\\\\033[38;5;252m/g") + done + + # Types + local types="${SYNTAX_TYPES[$lang]}" + for tp in $types; do + code=$(echo "$code" | sed "s/\b${tp}\b/\\\\033[38;5;39m${tp}\\\\033[38;5;252m/g") + done + + # Strings + code=$(echo "$code" | sed 's/"\([^"]*\)"/\\033[38;5;226m"\1"\\033[38;5;252m/g') + code=$(echo "$code" | sed "s/'\([^']*\)'/\\033[38;5;226m'\1'\\033[38;5;252m/g") + + # Numbers + code=$(echo "$code" | sed 's/\b\([0-9]\+\)\b/\\033[38;5;208m\1\\033[38;5;252m/g') + + # Comments + code=$(echo "$code" | sed 's/#\(.*\)$/\\033[38;5;240m#\1\\033[38;5;252m/g') + code=$(echo "$code" | sed 's|//\(.*\)$|\\033[38;5;240m//\1\\033[38;5;252m|g') + + echo "$code" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MARKDOWN PARSING +#─────────────────────────────────────────────────────────────────────────────── + +render_line() { + local line="$1" + local width="${2:-80}" + local in_code="$3" + local code_lang="$4" + + # Inside code block + if [[ "$in_code" == "true" ]]; then + local highlighted=$(highlight_code "$line" "$code_lang") + printf "${C_CODE_BLOCK} \033[38;5;252m%-$((width-4))s ${C_RESET}\n" "$line" + return + fi + + # Headers + if [[ "$line" =~ ^######[[:space:]](.+)$ ]]; then + printf "\n${C_H6} ${BASH_REMATCH[1]}${C_RESET}\n" + return + elif [[ "$line" =~ ^#####[[:space:]](.+)$ ]]; then + printf "\n${C_H5} ${BASH_REMATCH[1]}${C_RESET}\n" + return + elif [[ "$line" =~ ^####[[:space:]](.+)$ ]]; then + printf "\n${C_H4} ${BASH_REMATCH[1]}${C_RESET}\n" + return + elif [[ "$line" =~ ^###[[:space:]](.+)$ ]]; then + printf "\n${C_H3} ${BASH_REMATCH[1]}${C_RESET}\n" + return + elif [[ "$line" =~ ^##[[:space:]](.+)$ ]]; then + printf "\n${C_H2} ══ ${BASH_REMATCH[1]} ══${C_RESET}\n" + return + elif [[ "$line" =~ ^#[[:space:]](.+)$ ]]; then + local header="${BASH_REMATCH[1]}" + printf "\n${C_H1}╔" + printf '═%.0s' $(seq 1 $((width-2))) + printf "╗${C_RESET}\n" + printf "${C_H1}║ %-$((width-4))s ║${C_RESET}\n" "$header" + printf "${C_H1}╚" + printf '═%.0s' $(seq 1 $((width-2))) + printf "╝${C_RESET}\n" + return + fi + + # Horizontal rule + if [[ "$line" =~ ^[-*_]{3,}$ ]]; then + printf "${C_HR}" + printf '─%.0s' $(seq 1 $width) + printf "${C_RESET}\n" + return + fi + + # Blockquote + if [[ "$line" =~ ^\>[[:space:]]?(.*)$ ]]; then + local quote="${BASH_REMATCH[1]}" + printf "${C_QUOTE} ┃ ${C_ITALIC}%s${C_RESET}\n" "$quote" + return + fi + + # Unordered list + if [[ "$line" =~ ^[[:space:]]*[-*+][[:space:]](.+)$ ]]; then + local indent=$(echo "$line" | sed 's/[-*+].*//' | wc -c) + local item="${BASH_REMATCH[1]}" + item=$(render_inline "$item") + printf "%*s${C_LIST}●${C_RESET} %b\n" "$indent" "" "$item" + return + fi + + # Ordered list + if [[ "$line" =~ ^[[:space:]]*([0-9]+)\.[[:space:]](.+)$ ]]; then + local num="${BASH_REMATCH[1]}" + local item="${BASH_REMATCH[2]}" + item=$(render_inline "$item") + printf " ${C_LIST}%s.${C_RESET} %b\n" "$num" "$item" + return + fi + + # Checkbox + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]\[([xX\ ])\][[:space:]](.+)$ ]]; then + local checked="${BASH_REMATCH[1]}" + local item="${BASH_REMATCH[2]}" + if [[ "$checked" =~ [xX] ]]; then + printf " ${C_LIST}☑${C_RESET} ${C_DIM}${C_STRIKE}%s${C_RESET}\n" "$item" + else + printf " ${C_LIST}☐${C_RESET} %s\n" "$item" + fi + return + fi + + # Empty line + if [[ -z "$line" ]]; then + printf "\n" + return + fi + + # Regular paragraph + local rendered=$(render_inline "$line") + printf " %b\n" "$rendered" +} + +render_inline() { + local text="$1" + + # Images ![alt](url) + text=$(echo "$text" | sed -E "s/!\[([^]]*)\]\(([^)]+)\)/${C_IMAGE}[IMAGE: \1]${C_RESET}/g") + + # Links [text](url) + text=$(echo "$text" | sed -E "s/\[([^]]+)\]\(([^)]+)\)/${C_LINK}\1${C_RESET} ${C_LINK_URL}(\2)${C_RESET}/g") + + # Bold **text** or __text__ + text=$(echo "$text" | sed -E "s/\*\*([^*]+)\*\*/${C_BOLD}\1${C_RESET}/g") + text=$(echo "$text" | sed -E "s/__([^_]+)__/${C_BOLD}\1${C_RESET}/g") + + # Italic *text* or _text_ + text=$(echo "$text" | sed -E "s/\*([^*]+)\*/${C_ITALIC}\1${C_RESET}/g") + text=$(echo "$text" | sed -E "s/(? 0)) && ((scroll--)) ;; + '[B') ((scroll < total - display_lines)) && ((scroll++)) ;; + '[5') scroll=$((scroll - display_lines)); ((scroll < 0)) && scroll=0 ;; + '[6') scroll=$((scroll + display_lines)); ((scroll > total - display_lines)) && scroll=$((total - display_lines)) ;; + '[H') scroll=0 ;; + '[F') scroll=$((total - display_lines)) ;; + esac + ;; + q|Q) + tput cnorm + clear + exit 0 + ;; + esac + + # Bounds check + ((scroll < 0)) && scroll=0 + ((scroll > total - display_lines)) && scroll=$((total - display_lines)) + ((scroll < 0)) && scroll=0 + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'EOF' + + BLACKROAD MARKDOWN RENDERER + ═══════════════════════════ + + Usage: markdown-renderer.sh [options] + + Options: + -i, --interactive Open in interactive viewer + -w, --width Set output width (default: 80) + -h, --help Show this help + + Features: + • Headers (h1-h6) with styling + • Bold, italic, strikethrough + • Inline code and code blocks + • Syntax highlighting for code + • Links and images + • Ordered and unordered lists + • Checkboxes + • Blockquotes + • Tables + • Horizontal rules + + Examples: + markdown-renderer.sh README.md + markdown-renderer.sh -i README.md + markdown-renderer.sh -w 120 docs/guide.md + +EOF +} + +main() { + local file="" + local interactive=false + local width=80 + + while [[ $# -gt 0 ]]; do + case "$1" in + -i|--interactive) interactive=true; shift ;; + -w|--width) width="$2"; shift 2 ;; + -h|--help) show_help; exit 0 ;; + *) file="$1"; shift ;; + esac + done + + if [[ -z "$file" ]]; then + show_help + exit 1 + fi + + if [[ ! -f "$file" ]]; then + printf "\033[31mError: File not found: %s\033[0m\n" "$file" + exit 1 + fi + + if $interactive; then + interactive_viewer "$file" + else + render_markdown "$file" "$width" + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/matrix-rain.sh b/matrix-rain.sh new file mode 100644 index 0000000..e39238c --- /dev/null +++ b/matrix-rain.sh @@ -0,0 +1,516 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███╗ ███╗ █████╗ ████████╗██████╗ ██╗██╗ ██╗ +# ████╗ ████║██╔══██╗╚══██╔══╝██╔══██╗██║╚██╗██╔╝ +# ██╔████╔██║███████║ ██║ ██████╔╝██║ ╚███╔╝ +# ██║╚██╔╝██║██╔══██║ ██║ ██╔══██╗██║ ██╔██╗ +# ██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║██║██╔╝ ██╗ +# ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD MATRIX RAIN EFFECT v3.0 +# Digital Rain Animation with Multiple Modes +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +# Character sets +CHARS_MATRIX="アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン0123456789" +CHARS_BINARY="01" +CHARS_HEX="0123456789ABCDEF" +CHARS_CODE="{}[]()<>/*+-=:;.,!?@#$%^&|~" +CHARS_ALPHA="ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +# Current config +CHAR_SET="$CHARS_MATRIX" +COLOR_MODE="green" +DENSITY=0.6 +SPEED=0.03 +TRAIL_LENGTH=15 + +# Color schemes +declare -A COLOR_SCHEMES=( + [green]="0;255;65" + [cyan]="0;255;255" + [blue]="0;128;255" + [red]="255;0;65" + [purple]="180;0;255" + [orange]="255;128;0" + [rainbow]="rainbow" + [blackroad]="247;147;26" +) + +#─────────────────────────────────────────────────────────────────────────────── +# RAIN DROP SYSTEM +#─────────────────────────────────────────────────────────────────────────────── + +declare -a DROPS_X=() +declare -a DROPS_Y=() +declare -a DROPS_SPEED=() +declare -a DROPS_LENGTH=() +declare -a DROPS_CHARS=() + +# Initialize drops +init_drops() { + local width=$1 + local count=$2 + + DROPS_X=() + DROPS_Y=() + DROPS_SPEED=() + DROPS_LENGTH=() + DROPS_CHARS=() + + for ((i=0; i/dev/null; then + case "$key" in + q|Q) break ;; + +) SPEED=$(echo "$SPEED - 0.01" | bc -l 2>/dev/null || echo "$SPEED") ;; + -) SPEED=$(echo "$SPEED + 0.01" | bc -l 2>/dev/null || echo "$SPEED") ;; + c|C) + # Cycle colors + local colors=(green cyan blue red purple orange rainbow blackroad) + local current_idx=0 + for ((idx=0; idx<${#colors[@]}; idx++)); do + [[ "${colors[$idx]}" == "$COLOR_MODE" ]] && current_idx=$idx && break + done + COLOR_MODE="${colors[$(( (current_idx + 1) % ${#colors[@]} ))]}" + ;; + m|M) + # Cycle character sets + if [[ "$CHAR_SET" == "$CHARS_MATRIX" ]]; then + CHAR_SET="$CHARS_BINARY" + elif [[ "$CHAR_SET" == "$CHARS_BINARY" ]]; then + CHAR_SET="$CHARS_HEX" + elif [[ "$CHAR_SET" == "$CHARS_HEX" ]]; then + CHAR_SET="$CHARS_CODE" + else + CHAR_SET="$CHARS_MATRIX" + fi + ;; + esac + fi + done + + printf "\033[?25h" # Show cursor + printf "\033[0m" # Reset colors + clear +} + +# Matrix with text reveal +matrix_text_reveal() { + local text="${1:-BLACKROAD}" + local width=$(tput cols) + local height=$(tput lines) + + printf "\033[?25l" + printf "\033[2J" + printf "\033[40m" + + local text_x=$(( (width - ${#text}) / 2 )) + local text_y=$((height / 2)) + + local drop_count=$((width * DENSITY / 2)) + init_drops "$width" "$drop_count" + + local frame=0 + local reveal_start=100 + + trap "printf '\033[?25h\033[0m'; clear; exit" INT TERM + + while true; do + render_frame_fast "$width" "$height" + update_drops "$height" "$width" + + # Reveal text gradually + if [[ $frame -gt $reveal_start ]]; then + local chars_to_show=$(( (frame - reveal_start) / 5 )) + [[ $chars_to_show -gt ${#text} ]] && chars_to_show=${#text} + + printf "\033[%d;%dH" "$text_y" "$text_x" + printf "\033[38;2;255;255;255m\033[1m" + + for ((i=0; i/dev/null; then + [[ "$key" == "q" || "$key" == "Q" ]] && break + fi + done + + printf "\033[?25h" + printf "\033[0m" + clear +} + +# Matrix screensaver with stats +matrix_screensaver() { + local width=$(tput cols) + local height=$(tput lines) + local drop_count=$((width * DENSITY / 2)) + + printf "\033[?25l" + printf "\033[2J" + + init_drops "$width" "$drop_count" + + local start_time=$(date +%s) + local frame=0 + + trap "printf '\033[?25h\033[0m'; clear; exit" INT TERM + + while true; do + render_frame_fast "$width" "$height" + update_drops "$height" "$width" + + # Show stats in corner + local runtime=$(($(date +%s) - start_time)) + printf "\033[1;1H\033[38;2;50;50;50m" + printf "Frame: %d | Time: %ds | FPS: ~%d" "$frame" "$runtime" "$((frame / (runtime + 1)))" + + ((frame++)) + + if read -rsn1 -t "$SPEED" key 2>/dev/null; then + [[ "$key" == "q" || "$key" == "Q" ]] && break + fi + done + + printf "\033[?25h" + printf "\033[0m" + clear +} + +# Interactive matrix with mouse-like trails +matrix_interactive() { + local width=$(tput cols) + local height=$(tput lines) + + printf "\033[?25l" + printf "\033[2J" + printf "\033[40m" + + local drop_count=20 + init_drops "$width" "$drop_count" + + trap "printf '\033[?25h\033[0m'; clear; exit" INT TERM + + while true; do + render_frame_fast "$width" "$height" + update_drops "$height" "$width" + + if read -rsn1 -t 0.02 key 2>/dev/null; then + case "$key" in + q|Q) break ;; + # Add drops on keypress + *) + local new_x=$((RANDOM % width)) + DROPS_X+=("$new_x") + DROPS_Y+=("0") + DROPS_SPEED+=($((2 + RANDOM % 3))) + DROPS_LENGTH+=($((10 + RANDOM % 10))) + + local trail="" + for ((j=0; j<20; j++)); do + trail+="${CHAR_SET:$((RANDOM % ${#CHAR_SET})):1}" + done + DROPS_CHARS+=("$trail") + ;; + esac + fi + done + + printf "\033[?25h" + printf "\033[0m" + clear +} + +#─────────────────────────────────────────────────────────────────────────────── +# HELP +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'HELP' +BLACKROAD MATRIX RAIN + +Usage: matrix-rain.sh [mode] [options] + +Modes: + classic Classic matrix rain effect (default) + reveal Text reveal effect with matrix background + screensaver Screensaver mode with stats + interactive Interactive mode - press keys to add drops + +Controls: + q Quit + c Cycle colors (green, cyan, blue, red, purple, orange, rainbow) + m Cycle character sets (matrix, binary, hex, code) + +/- Speed up/slow down + +Options: + --color Set color (green, cyan, blue, red, purple, orange, rainbow, blackroad) + --speed Set speed (default: 0.03) + --density Set drop density (default: 0.6) + --chars Set characters (matrix, binary, hex, code, alpha) + +Examples: + matrix-rain.sh classic --color cyan + matrix-rain.sh reveal "WELCOME" + matrix-rain.sh screensaver --color rainbow + +HELP +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Parse arguments + mode="classic" + reveal_text="BLACKROAD" + + while [[ $# -gt 0 ]]; do + case "$1" in + classic|reveal|screensaver|interactive) + mode="$1" + shift + [[ "$mode" == "reveal" && -n "$1" && "${1:0:1}" != "-" ]] && { + reveal_text="$1" + shift + } + ;; + --color) + COLOR_MODE="$2" + shift 2 + ;; + --speed) + SPEED="$2" + shift 2 + ;; + --density) + DENSITY="$2" + shift 2 + ;; + --chars) + case "$2" in + matrix) CHAR_SET="$CHARS_MATRIX" ;; + binary) CHAR_SET="$CHARS_BINARY" ;; + hex) CHAR_SET="$CHARS_HEX" ;; + code) CHAR_SET="$CHARS_CODE" ;; + alpha) CHAR_SET="$CHARS_ALPHA" ;; + esac + shift 2 + ;; + -h|--help) + show_help + exit 0 + ;; + *) + shift + ;; + esac + done + + case "$mode" in + classic) matrix_classic ;; + reveal) matrix_text_reveal "$reveal_text" ;; + screensaver) matrix_screensaver ;; + interactive) matrix_interactive ;; + esac +fi diff --git a/music-visualizer.sh b/music-visualizer.sh new file mode 100644 index 0000000..46d6bc6 --- /dev/null +++ b/music-visualizer.sh @@ -0,0 +1,543 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███╗ ███╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗██╗███████╗ +# ████╗ ████║██║ ██║██╔════╝██║██╔════╝ ██║ ██║██║╚══███╔╝ +# ██╔████╔██║██║ ██║███████╗██║██║ ██║ ██║██║ ███╔╝ +# ██║╚██╔╝██║██║ ██║╚════██║██║██║ ╚██╗ ██╔╝██║ ███╔╝ +# ██║ ╚═╝ ██║╚██████╔╝███████║██║╚██████╗ ╚████╔╝ ██║███████╗ +# ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═════╝ ╚═══╝ ╚═╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD MUSIC VISUALIZER v3.0 +# ASCII Audio Visualization, Spectrum Analyzer & Beat Detection +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +VISUALIZER_WIDTH=80 +VISUALIZER_HEIGHT=20 +NUM_BARS=40 +SMOOTHING=0.3 + +# Visualization modes +MODE_BARS="bars" +MODE_WAVE="wave" +MODE_CIRCLE="circle" +MODE_SPECTRUM="spectrum" +MODE_PARTICLES="particles" + +CURRENT_MODE="$MODE_BARS" + +# Color schemes +declare -A COLOR_SCHEMES +COLOR_SCHEMES[rainbow]="196 202 208 214 220 226 190 154 118 82 46 47 48 49 50 51" +COLOR_SCHEMES[fire]="196 202 208 214 220 226 227 228 229 230 231" +COLOR_SCHEMES[ocean]="17 18 19 20 21 27 33 39 45 51 87 123 159" +COLOR_SCHEMES[matrix]="22 28 34 40 46 82 118 154 190 226" +COLOR_SCHEMES[purple]="53 54 55 56 57 93 129 165 201 207" +COLOR_SCHEMES[mono]="232 234 236 238 240 242 244 246 248 250 252 254 255" + +CURRENT_SCHEME="rainbow" + +#─────────────────────────────────────────────────────────────────────────────── +# AUDIO DATA SIMULATION (No audio input required) +#─────────────────────────────────────────────────────────────────────────────── + +# Previous values for smoothing +declare -a PREV_VALUES + +init_values() { + for ((i=0; i/dev/null || echo "$value") + fi + + # Clamp values + [[ $value -lt 0 ]] && value=0 + [[ $value -gt 100 ]] && value=100 + + values+=("$value") + PREV_VALUES[$i]=$value + done + + echo "${values[*]}" +} + +# Generate waveform data +generate_waveform() { + local phase="${1:-0}" + local -a values=() + + for ((i=0; i/dev/null || echo "0") + local wave1=$(echo "scale=4; s($angle) * 30 + 50" | bc -l 2>/dev/null || echo "50") + local wave2=$(echo "scale=4; s($angle * 2.5) * 15" | bc -l 2>/dev/null || echo "0") + local wave3=$(echo "scale=4; s($angle * 0.5) * 10" | bc -l 2>/dev/null || echo "0") + + local combined=$(echo "scale=0; $wave1 + $wave2 + $wave3 + ($RANDOM % 10)" | bc 2>/dev/null || echo "50") + + [[ $combined -lt 0 ]] && combined=0 + [[ $combined -gt 100 ]] && combined=100 + + values+=("$combined") + done + + echo "${values[*]}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# VISUALIZATION RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +get_color() { + local value="$1" + local max="${2:-100}" + local scheme="${3:-$CURRENT_SCHEME}" + + local colors=(${COLOR_SCHEMES[$scheme]}) + local num_colors=${#colors[@]} + + local idx=$(( value * (num_colors - 1) / max )) + [[ $idx -ge $num_colors ]] && idx=$((num_colors - 1)) + [[ $idx -lt 0 ]] && idx=0 + + echo "\033[38;5;${colors[$idx]}m" +} + +# Bar visualization +render_bars() { + local spectrum="$1" + local -a values=($spectrum) + + local bar_width=$((VISUALIZER_WIDTH / NUM_BARS)) + [[ $bar_width -lt 1 ]] && bar_width=1 + + # Draw from bottom to top + for ((row=VISUALIZER_HEIGHT; row>=1; row--)); do + local threshold=$((row * 100 / VISUALIZER_HEIGHT)) + + for ((i=0; i=1; row--)); do + local threshold=$((row * 100 / half_height)) + + for ((i=0; i/dev/null || echo "0") + + local spectrum_idx=$((p * NUM_BARS / num_points)) + local value="${values[$spectrum_idx]:-50}" + local radius=$(echo "scale=0; $base_radius + $value / 10" | bc 2>/dev/null || echo "$base_radius") + + local x=$(echo "scale=0; $center_x + c($angle) * $radius * 2" | bc -l 2>/dev/null || echo "$center_x") + local y=$(echo "scale=0; $center_y + s($angle) * $radius" | bc -l 2>/dev/null || echo "$center_y") + + x=${x%.*} + y=${y%.*} + + if [[ $x -ge 0 && $x -lt $VISUALIZER_WIDTH && $y -ge 0 && $y -lt $VISUALIZER_HEIGHT ]]; then + local color=$(get_color "$value") + grid[$((y * VISUALIZER_WIDTH + x))]="${color}●\033[0m" + fi + done + + # Render + for ((row=0; row/dev/null; then + case "$key" in + 1) CURRENT_MODE="$MODE_BARS" ;; + 2) CURRENT_MODE="$MODE_WAVE" ;; + 3) CURRENT_MODE="$MODE_SPECTRUM" ;; + 4) CURRENT_MODE="$MODE_CIRCLE" ;; + 5) CURRENT_MODE="$MODE_PARTICLES"; init_particles ;; + c|C) + local schemes=(rainbow fire ocean matrix purple mono) + local current_idx=0 + for ((i=0; i<${#schemes[@]}; i++)); do + [[ "${schemes[$i]}" == "$CURRENT_SCHEME" ]] && current_idx=$i + done + CURRENT_SCHEME="${schemes[$(( (current_idx + 1) % ${#schemes[@]} ))]}" + ;; + q|Q) + tput cnorm + clear + exit 0 + ;; + esac + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-run}" in + run) visualizer_loop ;; + bars) CURRENT_MODE="$MODE_BARS"; visualizer_loop ;; + wave) CURRENT_MODE="$MODE_WAVE"; visualizer_loop ;; + spectrum) CURRENT_MODE="$MODE_SPECTRUM"; visualizer_loop ;; + circle) CURRENT_MODE="$MODE_CIRCLE"; visualizer_loop ;; + particles) CURRENT_MODE="$MODE_PARTICLES"; visualizer_loop ;; + *) + printf "Usage: %s [run|bars|wave|spectrum|circle|particles]\n" "$0" + ;; + esac +fi diff --git a/network-topology.sh b/network-topology.sh new file mode 100644 index 0000000..616a988 --- /dev/null +++ b/network-topology.sh @@ -0,0 +1,698 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███╗ ██╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ +# ████╗ ██║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ +# ██╔██╗ ██║█████╗ ██║ ██║ █╗ ██║██║ ██║██████╔╝█████╔╝ +# ██║╚██╗██║██╔══╝ ██║ ██║███╗██║██║ ██║██╔══██╗██╔═██╗ +# ██║ ╚████║███████╗ ██║ ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ +# ╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD NETWORK TOPOLOGY VISUALIZER v3.0 +# ASCII Network Mapping, Discovery & Visualization +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# NETWORK DETECTION +#─────────────────────────────────────────────────────────────────────────────── + +get_local_ip() { + hostname -I 2>/dev/null | awk '{print $1}' || \ + ip route get 1 2>/dev/null | awk '{print $7}' | head -1 || \ + ifconfig 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1 | awk '{print $2}' +} + +get_gateway() { + ip route 2>/dev/null | grep default | awk '{print $3}' | head -1 || \ + netstat -rn 2>/dev/null | grep -E '^0\.0\.0\.0' | awk '{print $2}' | head -1 +} + +get_subnet() { + local ip=$(get_local_ip) + echo "${ip%.*}.0/24" +} + +get_interfaces() { + ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -v '^lo$' +} + +get_interface_info() { + local iface="$1" + local ip=$(ip -4 addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+') + local mac=$(ip link show "$iface" 2>/dev/null | grep -oP 'link/ether \K[\da-f:]+') + local state=$(ip link show "$iface" 2>/dev/null | grep -oP 'state \K\w+') + echo "$ip|$mac|$state" +} + +get_dns_servers() { + grep -E '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' +} + +get_public_ip() { + curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null || \ + curl -s --connect-timeout 3 https://ifconfig.me 2>/dev/null || \ + echo "unknown" +} + +#─────────────────────────────────────────────────────────────────────────────── +# NETWORK SCANNING +#─────────────────────────────────────────────────────────────────────────────── + +# Quick ping sweep +ping_sweep() { + local subnet="${1:-$(get_subnet)}" + local base="${subnet%.*}" + local results=() + + for i in {1..254}; do + local ip="${base}.$i" + ping -c 1 -W 1 "$ip" &>/dev/null && results+=("$ip") & + done + wait + + printf '%s\n' "${results[@]}" | sort -t. -k4 -n +} + +# ARP table scan +arp_scan() { + arp -a 2>/dev/null | grep -E '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' | \ + while read -r line; do + local hostname=$(echo "$line" | awk '{print $1}') + local ip=$(echo "$line" | grep -oP '\(\K[^\)]+') + local mac=$(echo "$line" | grep -oP 'at \K[\da-f:]+' || echo "??:??:??:??:??:??") + local iface=$(echo "$line" | awk '{print $NF}') + echo "$ip|$mac|$hostname|$iface" + done +} + +# Port scan (basic) +port_scan() { + local host="$1" + local ports="${2:-22,80,443,8080,3306,5432,27017,6379}" + local open_ports=() + + IFS=',' read -ra port_list <<< "$ports" + + for port in "${port_list[@]}"; do + (echo >/dev/tcp/"$host"/"$port") 2>/dev/null && open_ports+=("$port") + done + + echo "${open_ports[*]}" +} + +# Service detection +detect_service() { + local port="$1" + + case "$port" in + 22) echo "SSH" ;; + 80) echo "HTTP" ;; + 443) echo "HTTPS" ;; + 21) echo "FTP" ;; + 25) echo "SMTP" ;; + 53) echo "DNS" ;; + 110) echo "POP3" ;; + 143) echo "IMAP" ;; + 3306) echo "MySQL" ;; + 5432) echo "PostgreSQL" ;; + 27017) echo "MongoDB" ;; + 6379) echo "Redis" ;; + 8080) echo "HTTP-Alt" ;; + 8443) echo "HTTPS-Alt" ;; + 3000) echo "Node/Dev" ;; + 5000) echo "Flask" ;; + *) echo "Unknown" ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# TOPOLOGY DATA STRUCTURES +#─────────────────────────────────────────────────────────────────────────────── + +declare -A NETWORK_NODES +declare -A NODE_TYPES +declare -A NODE_CONNECTIONS + +# Node types +NODE_TYPE_ROUTER="router" +NODE_TYPE_SERVER="server" +NODE_TYPE_WORKSTATION="workstation" +NODE_TYPE_MOBILE="mobile" +NODE_TYPE_IOT="iot" +NODE_TYPE_UNKNOWN="unknown" + +# Add a node to topology +add_node() { + local id="$1" + local ip="$2" + local mac="$3" + local hostname="$4" + local type="${5:-$NODE_TYPE_UNKNOWN}" + + NETWORK_NODES[$id]="$ip|$mac|$hostname" + NODE_TYPES[$id]="$type" +} + +# Detect node type from MAC prefix +detect_node_type() { + local mac="$1" + local hostname="$2" + + # Check hostname patterns + [[ "$hostname" == *router* ]] && echo "$NODE_TYPE_ROUTER" && return + [[ "$hostname" == *gateway* ]] && echo "$NODE_TYPE_ROUTER" && return + [[ "$hostname" == *server* ]] && echo "$NODE_TYPE_SERVER" && return + [[ "$hostname" == *iphone* ]] && echo "$NODE_TYPE_MOBILE" && return + [[ "$hostname" == *android* ]] && echo "$NODE_TYPE_MOBILE" && return + [[ "$hostname" == *esp* ]] && echo "$NODE_TYPE_IOT" && return + + # Check MAC prefix (OUI) for common manufacturers + local prefix="${mac:0:8}" + + case "$prefix" in + "00:0c:29"|"00:50:56"|"00:1c:42") echo "$NODE_TYPE_SERVER" ;; # VMware/Parallels + "b8:27:eb"|"dc:a6:32") echo "$NODE_TYPE_IOT" ;; # Raspberry Pi + "00:17:88") echo "$NODE_TYPE_IOT" ;; # Philips IoT + *) echo "$NODE_TYPE_WORKSTATION" ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# ASCII TOPOLOGY RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +# Node icons +render_node_icon() { + local type="$1" + local status="${2:-up}" + + local color="$BR_GREEN" + [[ "$status" == "down" ]] && color="$BR_RED" + [[ "$status" == "unknown" ]] && color="$TEXT_MUTED" + + case "$type" in + router) + printf "${color}" + printf " ┌───┐ \n" + printf " ╔═╡ R ╞═╗ \n" + printf " ║ └───┘ ║ \n" + printf " ╚═══════╝ \n" + printf "${RST}" + ;; + server) + printf "${color}" + printf " ┌───────┐ \n" + printf " │ ═══ S │ \n" + printf " │ ═══ │ \n" + printf " └───────┘ \n" + printf "${RST}" + ;; + workstation) + printf "${color}" + printf " ┌─────┐ \n" + printf " │ ═══ │ \n" + printf " └──┬──┘ \n" + printf " ═══╧═══ \n" + printf "${RST}" + ;; + mobile) + printf "${color}" + printf " ┌───┐ \n" + printf " │ │ \n" + printf " │ ○ │ \n" + printf " └───┘ \n" + printf "${RST}" + ;; + iot) + printf "${color}" + printf " ╭─╮ \n" + printf " ╭╯ ╰╮ \n" + printf " │ ● │ \n" + printf " ╰───╯ \n" + printf "${RST}" + ;; + *) + printf "${color}" + printf " ┌─┐ \n" + printf " │?│ \n" + printf " └─┘ \n" + printf "${RST}" + ;; + esac +} + +# Render small node for topology map +render_small_node() { + local type="$1" + local status="${2:-up}" + + local color="$BR_GREEN" + [[ "$status" == "down" ]] && color="$BR_RED" + + case "$type" in + router) printf "${color}[R]${RST}" ;; + server) printf "${color}[S]${RST}" ;; + workstation) printf "${color}[W]${RST}" ;; + mobile) printf "${color}[M]${RST}" ;; + iot) printf "${color}[I]${RST}" ;; + internet) printf "${BR_CYAN}[☁]${RST}" ;; + *) printf "${color}[?]${RST}" ;; + esac +} + +# Render connection line +render_connection() { + local direction="$1" # h (horizontal), v (vertical), tr, tl, br, bl + + case "$direction" in + h) printf "════" ;; + v) printf "║" ;; + tr) printf "╗" ;; + tl) printf "╔" ;; + br) printf "╝" ;; + bl) printf "╚" ;; + t) printf "╦" ;; + b) printf "╩" ;; + l) printf "╠" ;; + r) printf "╣" ;; + x) printf "╬" ;; + *) printf " " ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# STAR TOPOLOGY (Common Home/Office Network) +#─────────────────────────────────────────────────────────────────────────────── + +render_star_topology() { + local gateway=$(get_gateway) + local local_ip=$(get_local_ip) + local public_ip=$(get_public_ip) + + clear_screen + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🌐 NETWORK TOPOLOGY - STAR ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Internet cloud + printf " ${BR_CYAN}╭──────────────╮${RST}\n" + printf " ${BR_CYAN}│ INTERNET │${RST}\n" + printf " ${BR_CYAN}│ %s │${RST}\n" "${public_ip:0:12}" + printf " ${BR_CYAN}╰──────┬───────╯${RST}\n" + printf " ${TEXT_MUTED}║${RST}\n" + printf " ${TEXT_MUTED}║${RST}\n" + + # Router/Gateway + printf " ${BR_ORANGE}┌───────────────┐${RST}\n" + printf " ${BR_ORANGE}│ ROUTER │${RST}\n" + printf " ${BR_ORANGE}│ %s │${RST}\n" "${gateway:-0.0.0.0}" + printf " ${BR_ORANGE}└───────┬───────┘${RST}\n" + printf " ${TEXT_MUTED}╔══════════════╬══════════════╗${RST}\n" + printf " ${TEXT_MUTED}║ ║ ║${RST}\n" + + # Discover devices + local devices=() + while IFS='|' read -r ip mac hostname iface; do + [[ -n "$ip" ]] && devices+=("$ip|$mac|$hostname") + done < <(arp_scan 2>/dev/null | head -6) + + # Render devices in a row + local device_count=${#devices[@]} + local positions=(0 1 2 3 4 5) + + for idx in "${!devices[@]}"; do + IFS='|' read -r ip mac hostname <<< "${devices[$idx]}" + local type=$(detect_node_type "$mac" "$hostname") + + local spacing=$((idx * 12 + 5)) + + if [[ "$ip" == "$local_ip" ]]; then + printf " ${BR_GREEN}┌─────────┐${RST}" + else + printf " ${BR_PURPLE}┌─────────┐${RST}" + fi + done + printf "\n" + + for idx in "${!devices[@]}"; do + IFS='|' read -r ip mac hostname <<< "${devices[$idx]}" + + if [[ "$ip" == "$local_ip" ]]; then + printf " ${BR_GREEN}│ THIS PC │${RST}" + else + printf " ${BR_PURPLE}│ %-7s │${RST}" "${hostname:0:7}" + fi + done + printf "\n" + + for idx in "${!devices[@]}"; do + IFS='|' read -r ip mac hostname <<< "${devices[$idx]}" + + if [[ "$ip" == "$local_ip" ]]; then + printf " ${BR_GREEN}│%-9s│${RST}" "${ip:0:9}" + else + printf " ${BR_PURPLE}│%-9s│${RST}" "${ip:0:9}" + fi + done + printf "\n" + + for idx in "${!devices[@]}"; do + if [[ "$ip" == "$local_ip" ]]; then + printf " ${BR_GREEN}└─────────┘${RST}" + else + printf " ${BR_PURPLE}└─────────┘${RST}" + fi + done + printf "\n\n" + + # Legend + printf "${TEXT_MUTED}Legend: ${BR_GREEN}● This Device${RST} ${BR_PURPLE}● Network Device${RST} ${BR_ORANGE}● Router/Gateway${RST} ${BR_CYAN}● Internet${RST}\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DETAILED DEVICE VIEW +#─────────────────────────────────────────────────────────────────────────────── + +render_device_details() { + local ip="$1" + + printf "\n${BR_CYAN}╔════════════════════════════════════════════════════════════╗${RST}\n" + printf "${BR_CYAN}║${RST} ${BOLD}Device Details: %s${RST}%*s${BR_CYAN}║${RST}\n" "$ip" $((42 - ${#ip})) "" + printf "${BR_CYAN}╠════════════════════════════════════════════════════════════╣${RST}\n" + + # Get MAC from ARP + local mac=$(arp -n "$ip" 2>/dev/null | grep -oP '[\da-f:]{17}' | head -1) + printf "${BR_CYAN}║${RST} MAC Address: %-42s ${BR_CYAN}║${RST}\n" "${mac:-Unknown}" + + # Hostname + local hostname=$(host "$ip" 2>/dev/null | awk '/domain name pointer/ {print $5}' | sed 's/\.$//') + printf "${BR_CYAN}║${RST} Hostname: %-42s ${BR_CYAN}║${RST}\n" "${hostname:-Unknown}" + + # Ping + local ping_result=$(ping -c 1 -W 1 "$ip" 2>/dev/null | grep -oP 'time=\K[\d.]+' || echo "timeout") + printf "${BR_CYAN}║${RST} Latency: %-42s ${BR_CYAN}║${RST}\n" "${ping_result}ms" + + # Open ports + printf "${BR_CYAN}║${RST} Open Ports: " + local ports=$(port_scan "$ip" "22,80,443,8080,3000,5000,3306,5432") + if [[ -n "$ports" ]]; then + printf "%-42s ${BR_CYAN}║${RST}\n" "$ports" + else + printf "%-42s ${BR_CYAN}║${RST}\n" "None detected" + fi + + printf "${BR_CYAN}╚════════════════════════════════════════════════════════════╝${RST}\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# INTERFACE STATUS +#─────────────────────────────────────────────────────────────────────────────── + +render_interface_status() { + printf "${BOLD}Network Interfaces${RST}\n\n" + + printf "%-15s %-18s %-20s %-10s\n" "INTERFACE" "IP ADDRESS" "MAC ADDRESS" "STATE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────" + + for iface in $(get_interfaces); do + IFS='|' read -r ip mac state <<< "$(get_interface_info "$iface")" + + local state_color="$BR_GREEN" + [[ "$state" != "UP" ]] && state_color="$BR_RED" + + printf "%-15s " "$iface" + printf "${BR_CYAN}%-18s${RST} " "${ip:-N/A}" + printf "${TEXT_MUTED}%-20s${RST} " "${mac:-N/A}" + printf "${state_color}%-10s${RST}\n" "${state:-DOWN}" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONNECTION MONITOR +#─────────────────────────────────────────────────────────────────────────────── + +render_connections() { + printf "${BOLD}Active Connections${RST}\n\n" + + printf "%-8s %-25s %-25s %-12s\n" "PROTO" "LOCAL" "REMOTE" "STATE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────────────────" + + netstat -tunapl 2>/dev/null | grep -E '^(tcp|udp)' | head -15 | while read -r proto recv send local remote state pid; do + local state_color="$TEXT_MUTED" + case "$state" in + ESTABLISHED) state_color="$BR_GREEN" ;; + LISTEN) state_color="$BR_CYAN" ;; + TIME_WAIT) state_color="$BR_YELLOW" ;; + CLOSE_WAIT) state_color="$BR_RED" ;; + esac + + printf "%-8s " "$proto" + printf "${BR_CYAN}%-25s${RST} " "${local:0:25}" + printf "${BR_PURPLE}%-25s${RST} " "${remote:0:25}" + printf "${state_color}%-12s${RST}\n" "${state:0:12}" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# BANDWIDTH MONITOR +#─────────────────────────────────────────────────────────────────────────────── + +monitor_bandwidth() { + local iface="${1:-$(get_interfaces | head -1)}" + local interval="${2:-1}" + + printf "${BOLD}Bandwidth Monitor: %s${RST}\n\n" "$iface" + + local rx_prev=$(cat /sys/class/net/"$iface"/statistics/rx_bytes 2>/dev/null || echo 0) + local tx_prev=$(cat /sys/class/net/"$iface"/statistics/tx_bytes 2>/dev/null || echo 0) + + for ((i=0; i<10; i++)); do + sleep "$interval" + + local rx_curr=$(cat /sys/class/net/"$iface"/statistics/rx_bytes 2>/dev/null || echo 0) + local tx_curr=$(cat /sys/class/net/"$iface"/statistics/tx_bytes 2>/dev/null || echo 0) + + local rx_rate=$(( (rx_curr - rx_prev) / 1024 )) + local tx_rate=$(( (tx_curr - tx_prev) / 1024 )) + + printf "\r ${BR_GREEN}↓${RST} RX: %6d KB/s ${BR_RED}↑${RST} TX: %6d KB/s " "$rx_rate" "$tx_rate" + + # Simple bar + local rx_bar=$((rx_rate / 100)) + local tx_bar=$((tx_rate / 100)) + [[ $rx_bar -gt 20 ]] && rx_bar=20 + [[ $tx_bar -gt 20 ]] && tx_bar=20 + + printf "${BR_GREEN}" + printf "%0.s█" $(seq 1 $rx_bar 2>/dev/null) || true + printf "${RST}" + printf " " + printf "${BR_RED}" + printf "%0.s█" $(seq 1 $tx_bar 2>/dev/null) || true + printf "${RST}" + + rx_prev=$rx_curr + tx_prev=$tx_curr + done + printf "\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DNS LOOKUP +#─────────────────────────────────────────────────────────────────────────────── + +dns_lookup() { + local domain="$1" + + printf "${BOLD}DNS Lookup: %s${RST}\n\n" "$domain" + + printf "${BR_CYAN}A Records:${RST}\n" + dig +short A "$domain" 2>/dev/null | while read -r ip; do + printf " %s\n" "$ip" + done + + printf "\n${BR_CYAN}AAAA Records:${RST}\n" + dig +short AAAA "$domain" 2>/dev/null | while read -r ip; do + printf " %s\n" "$ip" + done + + printf "\n${BR_CYAN}MX Records:${RST}\n" + dig +short MX "$domain" 2>/dev/null | while read -r mx; do + printf " %s\n" "$mx" + done + + printf "\n${BR_CYAN}NS Records:${RST}\n" + dig +short NS "$domain" 2>/dev/null | while read -r ns; do + printf " %s\n" "$ns" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# TRACEROUTE VISUALIZATION +#─────────────────────────────────────────────────────────────────────────────── + +visual_traceroute() { + local target="$1" + local max_hops="${2:-15}" + + printf "${BOLD}Traceroute to %s${RST}\n\n" "$target" + + local hop=0 + traceroute -n -m "$max_hops" -w 2 "$target" 2>/dev/null | while read -r line; do + if [[ "$line" == *"traceroute"* ]]; then + continue + fi + + ((hop++)) + + local ip=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + local time=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+ ms' | head -1) + + if [[ -n "$ip" ]]; then + printf " ${BR_CYAN}%2d${RST} ──▶ ${BR_GREEN}%-15s${RST} " "$hop" "$ip" + + # Latency bar + local ms=${time%% *} + local bar_len=$(printf "%.0f" "$(echo "$ms / 10" | bc -l 2>/dev/null || echo 1)") + [[ $bar_len -gt 30 ]] && bar_len=30 + [[ $bar_len -lt 1 ]] && bar_len=1 + + local bar_color="$BR_GREEN" + [[ ${ms%.*} -gt 50 ]] && bar_color="$BR_YELLOW" + [[ ${ms%.*} -gt 100 ]] && bar_color="$BR_RED" + + printf "%s" "$bar_color" + printf "%0.s█" $(seq 1 $bar_len 2>/dev/null) || true + printf "${RST} %s\n" "$time" + else + printf " ${BR_CYAN}%2d${RST} ──▶ ${TEXT_MUTED}* timeout${RST}\n" "$hop" + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_network_dashboard() { + clear_screen + cursor_hide + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🌐 NETWORK TOPOLOGY VISUALIZER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Network summary + local local_ip=$(get_local_ip) + local gateway=$(get_gateway) + local public_ip=$(get_public_ip) + local dns=$(get_dns_servers | head -1) + + printf " ${BOLD}Local IP:${RST} ${BR_GREEN}%s${RST} " "$local_ip" + printf "${BOLD}Gateway:${RST} ${BR_ORANGE}%s${RST} " "$gateway" + printf "${BOLD}Public IP:${RST} ${BR_CYAN}%s${RST}\n" "$public_ip" + printf " ${BOLD}DNS:${RST} ${TEXT_MUTED}%s${RST}\n\n" "$dns" + + # Interface status + render_interface_status + printf "\n" + + # Quick device scan + printf "${BOLD}Discovered Devices (ARP)${RST}\n\n" + printf "%-18s %-20s %-20s\n" "IP ADDRESS" "MAC ADDRESS" "HOSTNAME" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────" + + arp_scan 2>/dev/null | head -8 | while IFS='|' read -r ip mac hostname iface; do + [[ -z "$ip" ]] && continue + + local type=$(detect_node_type "$mac" "$hostname") + + printf " " + render_small_node "$type" + printf " ${BR_CYAN}%-15s${RST} " "$ip" + printf "${TEXT_MUTED}%-20s${RST} " "$mac" + printf "${TEXT_SECONDARY}%-20s${RST}\n" "${hostname:0:20}" + done + + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[t]opology [s]can [c]onnections [b]andwidth [d]ns [r]efresh [q]uit${RST}\n" +} + +network_dashboard_loop() { + while true; do + render_network_dashboard + + if read -rsn1 -t 5 key 2>/dev/null; then + case "$key" in + t|T) + render_star_topology + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + s|S) + printf "\n${BR_CYAN}Scanning network...${RST}\n" + ping_sweep | while read -r ip; do + printf " Found: %s\n" "$ip" + done + printf "${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + c|C) + clear_screen + render_connections + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + b|B) + clear_screen + monitor_bandwidth + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + d|D) + printf "\n${BR_CYAN}Domain to lookup: ${RST}" + cursor_show + read -r domain + cursor_hide + [[ -n "$domain" ]] && dns_lookup "$domain" + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + r|R) continue ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) network_dashboard_loop ;; + topology) render_star_topology ;; + interfaces) render_interface_status ;; + connections) render_connections ;; + scan) ping_sweep "${2:-}" ;; + arp) arp_scan ;; + ports) port_scan "$2" "${3:-22,80,443,8080}" ;; + bandwidth) monitor_bandwidth "$2" ;; + dns) dns_lookup "$2" ;; + trace) visual_traceroute "$2" ;; + device) render_device_details "$2" ;; + *) + printf "Usage: %s [dashboard|topology|interfaces|connections|scan|arp|ports|bandwidth|dns|trace|device]\n" "$0" + ;; + esac +fi diff --git a/notification-system.sh b/notification-system.sh new file mode 100644 index 0000000..13c5fd4 --- /dev/null +++ b/notification-system.sh @@ -0,0 +1,480 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███╗ ██╗ ██████╗ ████████╗██╗███████╗██╗ ██████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗ +# ████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██║██╔════╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝ +# ██╔██╗ ██║██║ ██║ ██║ ██║█████╗ ██║██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║███████╗ +# ██║╚██╗██║██║ ██║ ██║ ██║██╔══╝ ██║██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║ +# ██║ ╚████║╚██████╔╝ ██║ ██║██║ ██║╚██████╗██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████║ +# ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD NOTIFICATION SYSTEM v2.0 +# Multi-channel alerting: Terminal, Desktop, Sound, Webhooks +#═══════════════════════════════════════════════════════════════════════════════ + +# Source core library +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +NOTIFICATION_HOME="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/notifications" +NOTIFICATION_LOG="$NOTIFICATION_HOME/history.log" +NOTIFICATION_CONFIG="$NOTIFICATION_HOME/config.json" + +# Notification channels (can be combined) +NOTIFY_TERMINAL="${NOTIFY_TERMINAL:-1}" +NOTIFY_DESKTOP="${NOTIFY_DESKTOP:-1}" +NOTIFY_SOUND="${NOTIFY_SOUND:-1}" +NOTIFY_WEBHOOK="${NOTIFY_WEBHOOK:-0}" +NOTIFY_LOG="${NOTIFY_LOG:-1}" + +# Webhook URLs (optional) +SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" +DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}" +TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" +TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" + +# Rate limiting +NOTIFICATION_COOLDOWN=5 # Minimum seconds between notifications of same type +declare -A LAST_NOTIFICATION_TIME + +# Ensure directories exist +mkdir -p "$NOTIFICATION_HOME" 2>/dev/null + +#─────────────────────────────────────────────────────────────────────────────── +# NOTIFICATION TYPES +#─────────────────────────────────────────────────────────────────────────────── + +declare -A NOTIFICATION_TYPES=( + [info]="ℹ️ |INFO|$BR_CYAN|Pop" + [success]="✅|SUCCESS|$BR_GREEN|Glass" + [warning]="⚠️ |WARNING|$BR_YELLOW|Sosumi" + [error]="❌|ERROR|$BR_RED|Basso" + [critical]="🚨|CRITICAL|$BR_RED$BLINK|Funk" + [alert]="🔔|ALERT|$BR_ORANGE|Ping" + [update]="🔄|UPDATE|$BR_PURPLE|Purr" + [security]="🔒|SECURITY|$BR_PINK|Hero" + [deploy]="🚀|DEPLOY|$BR_GREEN|Submarine" + [payment]="💰|PAYMENT|$BR_YELLOW|Blow" +) + +#─────────────────────────────────────────────────────────────────────────────── +# CORE NOTIFICATION FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Main notification function +notify() { + local type="${1:-info}" + local title="$2" + local message="$3" + local channels="${4:-all}" # all, terminal, desktop, sound, webhook + + # Get notification config + local config="${NOTIFICATION_TYPES[$type]:-${NOTIFICATION_TYPES[info]}}" + IFS='|' read -r icon label color sound <<< "$config" + + # Rate limiting + local now=$(date +%s) + local key="${type}_${title}" + local last_time="${LAST_NOTIFICATION_TIME[$key]:-0}" + + if [[ $((now - last_time)) -lt $NOTIFICATION_COOLDOWN ]]; then + return 0 # Skip, too soon + fi + LAST_NOTIFICATION_TIME[$key]=$now + + # Dispatch to channels + case "$channels" in + all) + [[ "$NOTIFY_TERMINAL" == "1" ]] && notify_terminal "$icon" "$label" "$title" "$message" "$color" + [[ "$NOTIFY_DESKTOP" == "1" ]] && notify_desktop "$icon" "$title" "$message" "$type" + [[ "$NOTIFY_SOUND" == "1" ]] && notify_sound "$sound" + [[ "$NOTIFY_WEBHOOK" == "1" ]] && notify_webhooks "$type" "$title" "$message" + ;; + terminal) notify_terminal "$icon" "$label" "$title" "$message" "$color" ;; + desktop) notify_desktop "$icon" "$title" "$message" "$type" ;; + sound) notify_sound "$sound" ;; + webhook) notify_webhooks "$type" "$title" "$message" ;; + esac + + # Log notification + [[ "$NOTIFY_LOG" == "1" ]] && log_notification "$type" "$title" "$message" +} + +# Terminal notification (inline or popup style) +notify_terminal() { + local icon="$1" + local label="$2" + local title="$3" + local message="$4" + local color="$5" + + local timestamp=$(date "+%H:%M:%S") + + # Save cursor, print notification, restore cursor + printf "\n" + printf "${color}╭─────────────────────────────────────────────────────────────────╮${RST}\n" + printf "${color}│${RST} ${icon} ${BOLD}${color}${label}${RST} ${TEXT_MUTED}[${timestamp}]${RST}\n" + printf "${color}│${RST} ${BOLD}${title}${RST}\n" + [[ -n "$message" ]] && printf "${color}│${RST} ${TEXT_SECONDARY}${message}${RST}\n" + printf "${color}╰─────────────────────────────────────────────────────────────────╯${RST}\n" +} + +# Desktop notification (OS-specific) +notify_desktop() { + local icon="$1" + local title="$2" + local message="$3" + local type="$4" + + local os=$(get_os) + + case "$os" in + macos) + osascript -e "display notification \"$message\" with title \"$icon $title\" sound name \"default\"" 2>/dev/null & + ;; + linux) + if command -v notify-send &>/dev/null; then + local urgency="normal" + [[ "$type" == "critical" ]] && urgency="critical" + [[ "$type" == "error" ]] && urgency="critical" + + notify-send -u "$urgency" "$icon $title" "$message" 2>/dev/null & + fi + ;; + esac +} + +# Sound notification +notify_sound() { + local sound="$1" + + local os=$(get_os) + + case "$os" in + macos) + afplay "/System/Library/Sounds/${sound}.aiff" 2>/dev/null & + ;; + linux) + if command -v paplay &>/dev/null; then + paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null & + elif command -v aplay &>/dev/null; then + aplay /usr/share/sounds/sound-icons/prompt.wav 2>/dev/null & + fi + ;; + esac +} + +# Log notification to file +log_notification() { + local type="$1" + local title="$2" + local message="$3" + + local timestamp=$(date "+%Y-%m-%d %H:%M:%S") + printf "[%s] [%s] %s: %s\n" "$timestamp" "$type" "$title" "$message" >> "$NOTIFICATION_LOG" +} + +#─────────────────────────────────────────────────────────────────────────────── +# WEBHOOK INTEGRATIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Dispatch to all configured webhooks +notify_webhooks() { + local type="$1" + local title="$2" + local message="$3" + + [[ -n "$SLACK_WEBHOOK_URL" ]] && notify_slack "$type" "$title" "$message" & + [[ -n "$DISCORD_WEBHOOK_URL" ]] && notify_discord "$type" "$title" "$message" & + [[ -n "$TELEGRAM_BOT_TOKEN" ]] && notify_telegram "$type" "$title" "$message" & + + wait +} + +# Slack webhook +notify_slack() { + local type="$1" + local title="$2" + local message="$3" + + local color="#00d4ff" + case "$type" in + success) color="#14f195" ;; + warning) color="#ffd700" ;; + error|critical) color="#ff5252" ;; + esac + + local payload=$(cat << EOF +{ + "attachments": [{ + "color": "$color", + "title": "[$type] $title", + "text": "$message", + "footer": "BlackRoad Dashboards", + "ts": $(date +%s) + }] +} +EOF +) + + curl -s -X POST -H 'Content-type: application/json' \ + --data "$payload" "$SLACK_WEBHOOK_URL" >/dev/null 2>&1 +} + +# Discord webhook +notify_discord() { + local type="$1" + local title="$2" + local message="$3" + + local color=65535 # cyan + case "$type" in + success) color=1369587 ;; + warning) color=16766720 ;; + error|critical) color=16724530 ;; + esac + + local payload=$(cat << EOF +{ + "embeds": [{ + "title": "[$type] $title", + "description": "$message", + "color": $color, + "footer": { + "text": "BlackRoad Dashboards" + }, + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + }] +} +EOF +) + + curl -s -X POST -H 'Content-type: application/json' \ + --data "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null 2>&1 +} + +# Telegram notification +notify_telegram() { + local type="$1" + local title="$2" + local message="$3" + + [[ -z "$TELEGRAM_CHAT_ID" ]] && return 1 + + local icon="ℹ️" + case "$type" in + success) icon="✅" ;; + warning) icon="⚠️" ;; + error) icon="❌" ;; + critical) icon="🚨" ;; + esac + + local text="$icon *[$type]* $title +$message + +_BlackRoad Dashboards_" + + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d chat_id="$TELEGRAM_CHAT_ID" \ + -d text="$text" \ + -d parse_mode="Markdown" >/dev/null 2>&1 +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONVENIENCE FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +notify_info() { notify "info" "$1" "$2" "${3:-all}"; } +notify_success() { notify "success" "$1" "$2" "${3:-all}"; } +notify_warning() { notify "warning" "$1" "$2" "${3:-all}"; } +notify_error() { notify "error" "$1" "$2" "${3:-all}"; } +notify_critical() { notify "critical" "$1" "$2" "${3:-all}"; } +notify_alert() { notify "alert" "$1" "$2" "${3:-all}"; } +notify_update() { notify "update" "$1" "$2" "${3:-all}"; } +notify_security() { notify "security" "$1" "$2" "${3:-all}"; } +notify_deploy() { notify "deploy" "$1" "$2" "${3:-all}"; } +notify_payment() { notify "payment" "$1" "$2" "${3:-all}"; } + +#─────────────────────────────────────────────────────────────────────────────── +# NOTIFICATION CENTER (Interactive UI) +#─────────────────────────────────────────────────────────────────────────────── + +notification_center() { + local mode="${1:-view}" + + case "$mode" in + view) notification_center_view ;; + test) notification_center_test ;; + clear) notification_center_clear ;; + config) notification_center_config ;; + esac +} + +notification_center_view() { + clear_screen + cursor_hide + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🔔 BLACKROAD NOTIFICATION CENTER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Channel status + printf "${TEXT_SECONDARY}Active Channels:${RST} " + [[ "$NOTIFY_TERMINAL" == "1" ]] && printf "${BR_GREEN}Terminal${RST} " + [[ "$NOTIFY_DESKTOP" == "1" ]] && printf "${BR_GREEN}Desktop${RST} " + [[ "$NOTIFY_SOUND" == "1" ]] && printf "${BR_GREEN}Sound${RST} " + [[ "$NOTIFY_WEBHOOK" == "1" ]] && printf "${BR_GREEN}Webhooks${RST} " + printf "\n\n" + + # Recent notifications + printf "${BOLD}Recent Notifications:${RST}\n" + printf "${TEXT_MUTED}─────────────────────────────────────────────────────────────────${RST}\n" + + if [[ -f "$NOTIFICATION_LOG" ]]; then + tail -20 "$NOTIFICATION_LOG" | while IFS= read -r line; do + local type=$(echo "$line" | grep -oE '\[(INFO|SUCCESS|WARNING|ERROR|CRITICAL|ALERT)\]' | tr -d '[]') + local color="$BR_CYAN" + + case "$type" in + SUCCESS) color="$BR_GREEN" ;; + WARNING) color="$BR_YELLOW" ;; + ERROR|CRITICAL) color="$BR_RED" ;; + ALERT) color="$BR_ORANGE" ;; + esac + + printf "${color}%s${RST}\n" "$line" + done + else + printf "${TEXT_MUTED}No notifications yet.${RST}\n" + fi + + printf "\n${TEXT_SECONDARY}[t]est [c]lear [s]ettings [q]uit${RST}\n" + + # Handle input + while true; do + read -rsn1 key + case "$key" in + t|T) notification_center_test; notification_center_view ;; + c|C) notification_center_clear; notification_center_view ;; + s|S) notification_center_config ;; + q|Q) cursor_show; return ;; + esac + done +} + +notification_center_test() { + printf "\n${BR_PURPLE}Sending test notifications...${RST}\n" + + notify_info "Test Info" "This is an informational notification" + sleep 0.5 + notify_success "Test Success" "Operation completed successfully" + sleep 0.5 + notify_warning "Test Warning" "This is a warning notification" + sleep 0.5 + notify_error "Test Error" "Something went wrong" + + printf "${BR_GREEN}Test notifications sent!${RST}\n" + sleep 2 +} + +notification_center_clear() { + : > "$NOTIFICATION_LOG" + printf "${BR_GREEN}Notification log cleared.${RST}\n" + sleep 1 +} + +notification_center_config() { + clear_screen + + printf "${BR_CYAN}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════╗\n" + printf "║ ⚙️ NOTIFICATION SETTINGS ║\n" + printf "╚══════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + printf "${BOLD}Channel Configuration:${RST}\n\n" + + printf "1. Terminal Notifications: ${NOTIFY_TERMINAL:+${BR_GREEN}Enabled${RST}}${NOTIFY_TERMINAL:-${BR_RED}Disabled${RST}}\n" + printf "2. Desktop Notifications: ${NOTIFY_DESKTOP:+${BR_GREEN}Enabled${RST}}${NOTIFY_DESKTOP:-${BR_RED}Disabled${RST}}\n" + printf "3. Sound Notifications: ${NOTIFY_SOUND:+${BR_GREEN}Enabled${RST}}${NOTIFY_SOUND:-${BR_RED}Disabled${RST}}\n" + printf "4. Webhook Notifications: ${NOTIFY_WEBHOOK:+${BR_GREEN}Enabled${RST}}${NOTIFY_WEBHOOK:-${BR_RED}Disabled${RST}}\n" + printf "5. Logging: ${NOTIFY_LOG:+${BR_GREEN}Enabled${RST}}${NOTIFY_LOG:-${BR_RED}Disabled${RST}}\n" + + printf "\n${BOLD}Webhook Configuration:${RST}\n\n" + + printf "Slack: ${SLACK_WEBHOOK_URL:+${BR_GREEN}Configured${RST}}${SLACK_WEBHOOK_URL:-${TEXT_MUTED}Not configured${RST}}\n" + printf "Discord: ${DISCORD_WEBHOOK_URL:+${BR_GREEN}Configured${RST}}${DISCORD_WEBHOOK_URL:-${TEXT_MUTED}Not configured${RST}}\n" + printf "Telegram: ${TELEGRAM_BOT_TOKEN:+${BR_GREEN}Configured${RST}}${TELEGRAM_BOT_TOKEN:-${TEXT_MUTED}Not configured${RST}}\n" + + printf "\n${TEXT_SECONDARY}Press any key to return...${RST}" + read -rsn1 + notification_center_view +} + +#─────────────────────────────────────────────────────────────────────────────── +# SYSTEM MONITORING ALERTS +#─────────────────────────────────────────────────────────────────────────────── + +# Check system thresholds and notify +check_system_alerts() { + local cpu_threshold="${1:-80}" + local mem_threshold="${2:-85}" + local disk_threshold="${3:-90}" + + local cpu=$(get_cpu_usage) + local mem=$(get_memory_usage) + local disk=$(get_disk_usage "/") + + [[ $cpu -gt $cpu_threshold ]] && \ + notify_warning "High CPU Usage" "CPU usage is at ${cpu}% (threshold: ${cpu_threshold}%)" + + [[ $mem -gt $mem_threshold ]] && \ + notify_warning "High Memory Usage" "Memory usage is at ${mem}% (threshold: ${mem_threshold}%)" + + [[ $disk -gt $disk_threshold ]] && \ + notify_critical "Disk Space Low" "Disk usage is at ${disk}% (threshold: ${disk_threshold}%)" +} + +# Watch for service health +watch_service_health() { + local service_name="$1" + local check_command="$2" + local interval="${3:-60}" + + while true; do + if ! eval "$check_command" >/dev/null 2>&1; then + notify_critical "Service Down" "$service_name is not responding" + fi + sleep "$interval" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-center}" in + center) notification_center "view" ;; + test) notification_center_test ;; + clear) notification_center_clear ;; + config) notification_center_config ;; + notify) notify "$2" "$3" "$4" "${5:-all}" ;; + info) notify_info "$2" "$3" ;; + success) notify_success "$2" "$3" ;; + warning) notify_warning "$2" "$3" ;; + error) notify_error "$2" "$3" ;; + critical) notify_critical "$2" "$3" ;; + watch) check_system_alerts "$2" "$3" "$4" ;; + *) + printf "Usage: %s [center|test|clear|config|notify|watch]\n" "$0" + printf " %s notify <message>\n" "$0" + printf " %s info|success|warning|error|critical <title> <message>\n" "$0" + ;; + esac +fi diff --git a/password-generator.sh b/password-generator.sh new file mode 100644 index 0000000..405a32d --- /dev/null +++ b/password-generator.sh @@ -0,0 +1,666 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ █████╗ ███████╗███████╗██╗ ██╗ ██████╗ ██████╗ ██████╗ +# ██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██║██╔═══██╗██╔══██╗██╔══██╗ +# ██████╔╝███████║███████╗███████╗██║ █╗ ██║██║ ██║██████╔╝██║ ██║ +# ██╔═══╝ ██╔══██║╚════██║╚════██║██║███╗██║██║ ██║██╔══██╗██║ ██║ +# ██║ ██║ ██║███████║███████║╚███╔███╔╝╚██████╔╝██║ ██║██████╔╝ +# ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD PASSWORD GENERATOR v1.0 +# Secure Password Generation & Strength Analysis +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CHARACTER SETS +#─────────────────────────────────────────────────────────────────────────────── + +LOWERCASE="abcdefghijklmnopqrstuvwxyz" +UPPERCASE="ABCDEFGHIJKLMNOPQRSTUVWXYZ" +DIGITS="0123456789" +SYMBOLS="!@#\$%^&*()_+-=[]{}|;:,.<>?" +EXTENDED_SYMBOLS="\`~'\"\\/?" +SIMILAR="il1Lo0O" +AMBIGUOUS="{}[]()/\\'\"\`~,;:.<>" + +# Wordlists for passphrases +WORDS=( + "apple" "banana" "cherry" "dragon" "eagle" "falcon" "galaxy" "harbor" + "island" "jungle" "knight" "legend" "mountain" "nebula" "ocean" "phoenix" + "quartz" "rainbow" "shadow" "thunder" "universe" "volcano" "whisper" "xylophone" + "yellow" "zenith" "arctic" "blizzard" "crystal" "diamond" "eclipse" "fortress" + "glacier" "horizon" "infinity" "journey" "kingdom" "lighthouse" "midnight" "northstar" + "olympus" "paradise" "quantum" "riddle" "sapphire" "twilight" "ultimate" "velocity" + "warrior" "xenon" "yearning" "zephyr" "adventure" "beacon" "cascade" "destiny" +) + +#─────────────────────────────────────────────────────────────────────────────── +# PASSWORD GENERATION +#─────────────────────────────────────────────────────────────────────────────── + +generate_password() { + local length="${1:-16}" + local include_lower="${2:-true}" + local include_upper="${3:-true}" + local include_digits="${4:-true}" + local include_symbols="${5:-true}" + local exclude_similar="${6:-false}" + local exclude_ambiguous="${7:-false}" + + local charset="" + + [[ "$include_lower" == "true" ]] && charset+="$LOWERCASE" + [[ "$include_upper" == "true" ]] && charset+="$UPPERCASE" + [[ "$include_digits" == "true" ]] && charset+="$DIGITS" + [[ "$include_symbols" == "true" ]] && charset+="$SYMBOLS" + + # Remove similar characters + if [[ "$exclude_similar" == "true" ]]; then + for c in $(echo "$SIMILAR" | fold -w1); do + charset="${charset//$c/}" + done + fi + + # Remove ambiguous characters + if [[ "$exclude_ambiguous" == "true" ]]; then + for c in $(echo "$AMBIGUOUS" | fold -w1); do + charset="${charset//$c/}" + done + fi + + local charset_len=${#charset} + local password="" + + for ((i=0; i<length; i++)); do + local rand=$((RANDOM % charset_len)) + password+="${charset:$rand:1}" + done + + # Ensure at least one of each required type + local has_lower=false has_upper=false has_digit=false has_symbol=false + + for ((i=0; i<${#password}; i++)); do + local c="${password:$i:1}" + [[ "$LOWERCASE" == *"$c"* ]] && has_lower=true + [[ "$UPPERCASE" == *"$c"* ]] && has_upper=true + [[ "$DIGITS" == *"$c"* ]] && has_digit=true + [[ "$SYMBOLS" == *"$c"* ]] && has_symbol=true + done + + # Regenerate if missing required types + if [[ "$include_lower" == "true" && "$has_lower" == "false" ]] || + [[ "$include_upper" == "true" && "$has_upper" == "false" ]] || + [[ "$include_digits" == "true" && "$has_digit" == "false" ]] || + [[ "$include_symbols" == "true" && "$has_symbol" == "false" ]]; then + generate_password "$length" "$include_lower" "$include_upper" "$include_digits" "$include_symbols" "$exclude_similar" "$exclude_ambiguous" + return + fi + + echo "$password" +} + +generate_pin() { + local length="${1:-4}" + local pin="" + + for ((i=0; i<length; i++)); do + pin+=$((RANDOM % 10)) + done + + echo "$pin" +} + +generate_passphrase() { + local word_count="${1:-4}" + local separator="${2:--}" + local capitalize="${3:-true}" + + local passphrase="" + local word_pool_size=${#WORDS[@]} + + for ((i=0; i<word_count; i++)); do + local idx=$((RANDOM % word_pool_size)) + local word="${WORDS[$idx]}" + + if [[ "$capitalize" == "true" ]]; then + word="${word^}" + fi + + [[ $i -gt 0 ]] && passphrase+="$separator" + passphrase+="$word" + done + + echo "$passphrase" +} + +generate_pronounceable() { + local syllables="${1:-4}" + local consonants="bcdfghjklmnpqrstvwxyz" + local vowels="aeiou" + local password="" + + for ((s=0; s<syllables; s++)); do + local c_idx=$((RANDOM % ${#consonants})) + local v_idx=$((RANDOM % ${#vowels})) + password+="${consonants:$c_idx:1}" + password+="${vowels:$v_idx:1}" + + # Sometimes add another consonant + if [[ $((RANDOM % 2)) -eq 0 ]]; then + c_idx=$((RANDOM % ${#consonants})) + password+="${consonants:$c_idx:1}" + fi + done + + echo "$password" +} + +generate_pattern() { + local pattern="$1" + local result="" + + for ((i=0; i<${#pattern}; i++)); do + local c="${pattern:$i:1}" + case "$c" in + l) result+="${LOWERCASE:$((RANDOM % 26)):1}" ;; + L) result+="${UPPERCASE:$((RANDOM % 26)):1}" ;; + d) result+="${DIGITS:$((RANDOM % 10)):1}" ;; + s) result+="${SYMBOLS:$((RANDOM % ${#SYMBOLS})):1}" ;; + a) result+="$LOWERCASE$UPPERCASE"; result="${result:$((RANDOM % 52)):1}" ;; + *) result+="$c" ;; + esac + done + + echo "$result" +} + +#─────────────────────────────────────────────────────────────────────────────── +# PASSWORD STRENGTH ANALYSIS +#─────────────────────────────────────────────────────────────────────────────── + +analyze_strength() { + local password="$1" + local length=${#password} + + local has_lower=false has_upper=false has_digit=false has_symbol=false + local unique_chars="" + local consecutive=0 + local max_consecutive=0 + + local prev_char="" + + for ((i=0; i<length; i++)); do + local c="${password:$i:1}" + + [[ "$LOWERCASE" == *"$c"* ]] && has_lower=true + [[ "$UPPERCASE" == *"$c"* ]] && has_upper=true + [[ "$DIGITS" == *"$c"* ]] && has_digit=true + [[ "$SYMBOLS$EXTENDED_SYMBOLS" == *"$c"* ]] && has_symbol=true + + [[ "$unique_chars" != *"$c"* ]] && unique_chars+="$c" + + if [[ "$c" == "$prev_char" ]]; then + ((consecutive++)) + [[ $consecutive -gt $max_consecutive ]] && max_consecutive=$consecutive + else + consecutive=0 + fi + prev_char="$c" + done + + # Calculate entropy + local charset_size=0 + $has_lower && ((charset_size += 26)) + $has_upper && ((charset_size += 26)) + $has_digit && ((charset_size += 10)) + $has_symbol && ((charset_size += 32)) + + local entropy=0 + if [[ $charset_size -gt 0 ]]; then + entropy=$(echo "l($charset_size)/l(2) * $length" | bc -l 2>/dev/null || echo "0") + entropy=$(printf "%.0f" "$entropy") + fi + + # Calculate score (0-100) + local score=0 + + # Length score (up to 30 points) + if [[ $length -ge 16 ]]; then score=$((score + 30)) + elif [[ $length -ge 12 ]]; then score=$((score + 25)) + elif [[ $length -ge 8 ]]; then score=$((score + 15)) + else score=$((score + length)) + fi + + # Character variety (up to 40 points) + $has_lower && ((score += 10)) + $has_upper && ((score += 10)) + $has_digit && ((score += 10)) + $has_symbol && ((score += 10)) + + # Uniqueness (up to 20 points) + local unique_ratio=$((${#unique_chars} * 20 / length)) + score=$((score + unique_ratio)) + + # Entropy bonus (up to 10 points) + [[ $entropy -ge 60 ]] && score=$((score + 10)) + [[ $entropy -ge 40 && $entropy -lt 60 ]] && score=$((score + 5)) + + # Penalties + [[ $max_consecutive -ge 3 ]] && score=$((score - 10)) + [[ $length -lt 8 ]] && score=$((score - 20)) + + [[ $score -lt 0 ]] && score=0 + [[ $score -gt 100 ]] && score=100 + + # Determine strength level + local level="" + local color="" + if [[ $score -ge 80 ]]; then + level="EXCELLENT" + color="46" + elif [[ $score -ge 60 ]]; then + level="STRONG" + color="39" + elif [[ $score -ge 40 ]]; then + level="MODERATE" + color="226" + elif [[ $score -ge 20 ]]; then + level="WEAK" + color="208" + else + level="VERY WEAK" + color="196" + fi + + # Output analysis + echo "score:$score" + echo "level:$level" + echo "color:$color" + echo "length:$length" + echo "entropy:$entropy" + echo "unique:${#unique_chars}" + echo "has_lower:$has_lower" + echo "has_upper:$has_upper" + echo "has_digit:$has_digit" + echo "has_symbol:$has_symbol" +} + +render_strength() { + local password="$1" + + declare -A analysis + while IFS=: read -r key value; do + analysis[$key]="$value" + done < <(analyze_strength "$password") + + printf "\n \033[1;38;5;201m🔐 Password Strength Analysis\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + # Strength bar + local score=${analysis[score]} + local color=${analysis[color]} + local bar_width=40 + local filled=$((score * bar_width / 100)) + + printf " Strength: \033[38;5;${color}m" + for ((i=0; i<filled; i++)); do printf "█"; done + printf "\033[38;5;240m" + for ((i=filled; i<bar_width; i++)); do printf "░"; done + printf "\033[0m \033[38;5;${color}m%d%%\033[0m \033[1;38;5;${color}m%s\033[0m\n" "$score" "${analysis[level]}" + + printf "\n" + + # Details + printf " 📏 Length: %d characters\n" "${analysis[length]}" + printf " 🎲 Entropy: %d bits\n" "${analysis[entropy]}" + printf " 🔤 Unique: %d characters\n" "${analysis[unique]}" + + printf "\n Character Types:\n" + [[ "${analysis[has_lower]}" == "true" ]] && printf " \033[38;5;46m✓\033[0m Lowercase\n" || printf " \033[38;5;196m✗\033[0m Lowercase\n" + [[ "${analysis[has_upper]}" == "true" ]] && printf " \033[38;5;46m✓\033[0m Uppercase\n" || printf " \033[38;5;196m✗\033[0m Uppercase\n" + [[ "${analysis[has_digit]}" == "true" ]] && printf " \033[38;5;46m✓\033[0m Numbers\n" || printf " \033[38;5;196m✗\033[0m Numbers\n" + [[ "${analysis[has_symbol]}" == "true" ]] && printf " \033[38;5;46m✓\033[0m Symbols\n" || printf " \033[38;5;196m✗\033[0m Symbols\n" + + # Crack time estimation + local entropy=${analysis[entropy]} + local crack_seconds=$(echo "2^$entropy / 1000000000" | bc 2>/dev/null || echo "0") + + printf "\n ⏱️ Time to crack: " + if [[ $crack_seconds -ge 31536000000 ]]; then + printf "\033[38;5;46mCenturies\033[0m\n" + elif [[ $crack_seconds -ge 31536000 ]]; then + local years=$((crack_seconds / 31536000)) + printf "\033[38;5;46m%d years\033[0m\n" "$years" + elif [[ $crack_seconds -ge 86400 ]]; then + local days=$((crack_seconds / 86400)) + printf "\033[38;5;226m%d days\033[0m\n" "$days" + elif [[ $crack_seconds -ge 3600 ]]; then + local hours=$((crack_seconds / 3600)) + printf "\033[38;5;208m%d hours\033[0m\n" "$hours" + else + printf "\033[38;5;196mInstantly\033[0m\n" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# INTERACTIVE MODE +#─────────────────────────────────────────────────────────────────────────────── + +render_header() { + printf "\033[1;38;5;214m" + cat << 'EOF' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 🔐 PASSWORD GENERATOR SUITE ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +EOF + printf "\033[0m" +} + +render_menu() { + printf "\n \033[1;38;5;39mGeneration Options:\033[0m\n\n" + + printf " \033[38;5;46m1\033[0m 🔑 Random Password (customizable)\n" + printf " \033[38;5;46m2\033[0m 📝 Passphrase (memorable words)\n" + printf " \033[38;5;46m3\033[0m 🔢 PIN Code\n" + printf " \033[38;5;46m4\033[0m 🗣️ Pronounceable Password\n" + printf " \033[38;5;46m5\033[0m 🎯 Pattern-based Password\n" + printf " \033[38;5;46m6\033[0m 📊 Analyze Password Strength\n" + printf " \033[38;5;46m7\033[0m 📋 Batch Generate\n" + printf " \n" + printf " \033[38;5;240mq\033[0m Quit\n" +} + +interactive_random_password() { + printf "\n \033[1;38;5;201m🔑 Random Password Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Length (default 16): " + read -r length + length="${length:-16}" + + printf " Include lowercase? (Y/n): " + read -r lower + local inc_lower="true" + [[ "$lower" =~ ^[nN]$ ]] && inc_lower="false" + + printf " Include uppercase? (Y/n): " + read -r upper + local inc_upper="true" + [[ "$upper" =~ ^[nN]$ ]] && inc_upper="false" + + printf " Include numbers? (Y/n): " + read -r digits + local inc_digits="true" + [[ "$digits" =~ ^[nN]$ ]] && inc_digits="false" + + printf " Include symbols? (Y/n): " + read -r symbols + local inc_symbols="true" + [[ "$symbols" =~ ^[nN]$ ]] && inc_symbols="false" + + printf " Exclude similar (il1Lo0O)? (y/N): " + read -r similar + local exc_similar="false" + [[ "$similar" =~ ^[yY]$ ]] && exc_similar="true" + + printf "\n \033[1;38;5;46mGenerated Password:\033[0m\n\n" + + local password=$(generate_password "$length" "$inc_lower" "$inc_upper" "$inc_digits" "$inc_symbols" "$exc_similar" "false") + + printf " \033[48;5;236m \033[1;38;5;226m%s\033[0m\033[48;5;236m \033[0m\n" "$password" + + render_strength "$password" + + # Copy to clipboard if available + if command -v xclip &>/dev/null; then + echo -n "$password" | xclip -selection clipboard + printf "\n \033[38;5;46m✓ Copied to clipboard\033[0m\n" + elif command -v pbcopy &>/dev/null; then + echo -n "$password" | pbcopy + printf "\n \033[38;5;46m✓ Copied to clipboard\033[0m\n" + fi +} + +interactive_passphrase() { + printf "\n \033[1;38;5;201m📝 Passphrase Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Number of words (default 4): " + read -r count + count="${count:-4}" + + printf " Separator (default -): " + read -r sep + sep="${sep:--}" + + printf " Capitalize words? (Y/n): " + read -r cap + local capitalize="true" + [[ "$cap" =~ ^[nN]$ ]] && capitalize="false" + + local passphrase=$(generate_passphrase "$count" "$sep" "$capitalize") + + printf "\n \033[1;38;5;46mGenerated Passphrase:\033[0m\n\n" + printf " \033[48;5;236m \033[1;38;5;226m%s\033[0m\033[48;5;236m \033[0m\n" "$passphrase" + + render_strength "$passphrase" +} + +interactive_pin() { + printf "\n \033[1;38;5;201m🔢 PIN Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " PIN length (default 4): " + read -r length + length="${length:-4}" + + local pin=$(generate_pin "$length") + + printf "\n \033[1;38;5;46mGenerated PIN:\033[0m\n\n" + printf " \033[48;5;236m \033[1;38;5;226m%s\033[0m\033[48;5;236m \033[0m\n" "$pin" + + printf "\n \033[38;5;240m⚠️ PINs are weak - use only when required\033[0m\n" +} + +interactive_pronounceable() { + printf "\n \033[1;38;5;201m🗣️ Pronounceable Password Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Number of syllables (default 4): " + read -r syllables + syllables="${syllables:-4}" + + local password=$(generate_pronounceable "$syllables") + + printf "\n \033[1;38;5;46mGenerated Password:\033[0m\n\n" + printf " \033[48;5;236m \033[1;38;5;226m%s\033[0m\033[48;5;236m \033[0m\n" "$password" + + render_strength "$password" +} + +interactive_pattern() { + printf "\n \033[1;38;5;201m🎯 Pattern-based Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Pattern legend:\n" + printf " \033[38;5;39ml\033[0m = lowercase \033[38;5;39mL\033[0m = uppercase\n" + printf " \033[38;5;39md\033[0m = digit \033[38;5;39ms\033[0m = symbol\n" + printf " \033[38;5;39ma\033[0m = any letter \033[38;5;240mother\033[0m = literal\n" + printf "\n Example: LLLLdddd-ssss\n" + printf "\n Enter pattern: " + read -r pattern + + local password=$(generate_pattern "$pattern") + + printf "\n \033[1;38;5;46mGenerated Password:\033[0m\n\n" + printf " \033[48;5;236m \033[1;38;5;226m%s\033[0m\033[48;5;236m \033[0m\n" "$password" + + render_strength "$password" +} + +interactive_analyze() { + printf "\n \033[1;38;5;201m📊 Password Strength Analyzer\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Enter password to analyze: " + read -rs password + printf "\n" + + render_strength "$password" +} + +interactive_batch() { + printf "\n \033[1;38;5;201m📋 Batch Password Generator\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + printf " Number of passwords (default 10): " + read -r count + count="${count:-10}" + + printf " Length (default 16): " + read -r length + length="${length:-16}" + + printf "\n \033[1;38;5;46mGenerated Passwords:\033[0m\n\n" + + for ((i=1; i<=count; i++)); do + local pwd=$(generate_password "$length" "true" "true" "true" "true" "false" "false") + declare -A analysis + while IFS=: read -r key value; do + analysis[$key]="$value" + done < <(analyze_strength "$pwd") + + local color=${analysis[color]} + printf " %2d. \033[38;5;252m%s\033[0m \033[38;5;${color}m[%s]\033[0m\n" "$i" "$pwd" "${analysis[level]}" + done +} + +interactive_mode() { + while true; do + clear + render_header + render_menu + + printf "\n Select option: " + read -rsn1 key + + case "$key" in + 1) interactive_random_password ;; + 2) interactive_passphrase ;; + 3) interactive_pin ;; + 4) interactive_pronounceable ;; + 5) interactive_pattern ;; + 6) interactive_analyze ;; + 7) interactive_batch ;; + q|Q) clear; exit 0 ;; + *) continue ;; + esac + + printf "\n \033[38;5;240mPress any key to continue...\033[0m" + read -rsn1 + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'EOF' + + BLACKROAD PASSWORD GENERATOR SUITE + ═══════════════════════════════════ + + Usage: password-generator.sh [options] + + Options: + -l, --length <n> Password length (default: 16) + -c, --count <n> Generate multiple passwords + -p, --passphrase <n> Generate passphrase with n words + --pin <n> Generate PIN with n digits + --pattern <p> Generate from pattern (l=lower, L=upper, d=digit, s=symbol) + --analyze <pwd> Analyze password strength + -i, --interactive Interactive mode + -h, --help Show this help + + Examples: + password-generator.sh # Interactive mode + password-generator.sh -l 24 # 24-char password + password-generator.sh -c 5 # Generate 5 passwords + password-generator.sh -p 5 # 5-word passphrase + password-generator.sh --pin 6 # 6-digit PIN + password-generator.sh --pattern "LLLddd" # Pattern-based + +EOF +} + +main() { + local length=16 + local count=1 + local passphrase_words=0 + local pin_length=0 + local pattern="" + local analyze_pwd="" + local interactive=false + + if [[ $# -eq 0 ]]; then + interactive=true + fi + + while [[ $# -gt 0 ]]; do + case "$1" in + -l|--length) length="$2"; shift 2 ;; + -c|--count) count="$2"; shift 2 ;; + -p|--passphrase) passphrase_words="$2"; shift 2 ;; + --pin) pin_length="$2"; shift 2 ;; + --pattern) pattern="$2"; shift 2 ;; + --analyze) analyze_pwd="$2"; shift 2 ;; + -i|--interactive) interactive=true; shift ;; + -h|--help) show_help; exit 0 ;; + *) shift ;; + esac + done + + if $interactive; then + interactive_mode + exit 0 + fi + + if [[ -n "$analyze_pwd" ]]; then + render_strength "$analyze_pwd" + exit 0 + fi + + if [[ $passphrase_words -gt 0 ]]; then + for ((i=0; i<count; i++)); do + generate_passphrase "$passphrase_words" + done + exit 0 + fi + + if [[ $pin_length -gt 0 ]]; then + for ((i=0; i<count; i++)); do + generate_pin "$pin_length" + done + exit 0 + fi + + if [[ -n "$pattern" ]]; then + for ((i=0; i<count; i++)); do + generate_pattern "$pattern" + done + exit 0 + fi + + # Default: generate random passwords + for ((i=0; i<count; i++)); do + generate_password "$length" + done +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/plugin-system.sh b/plugin-system.sh new file mode 100644 index 0000000..f70d448 --- /dev/null +++ b/plugin-system.sh @@ -0,0 +1,523 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██╗ ██╗ ██╗ ██████╗ ██╗███╗ ██╗███████╗ +# ██╔══██╗██║ ██║ ██║██╔════╝ ██║████╗ ██║██╔════╝ +# ██████╔╝██║ ██║ ██║██║ ███╗██║██╔██╗ ██║███████╗ +# ██╔═══╝ ██║ ██║ ██║██║ ██║██║██║╚██╗██║╚════██║ +# ██║ ███████╗╚██████╔╝╚██████╔╝██║██║ ╚████║███████║ +# ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD PLUGIN SYSTEM v3.0 +# Modular Extension Architecture with Hot-loading & Sandboxing +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +PLUGIN_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/plugins" +PLUGIN_REGISTRY="$PLUGIN_DIR/registry.json" +PLUGIN_HOOKS="$PLUGIN_DIR/hooks" +PLUGIN_CONFIGS="$PLUGIN_DIR/configs" +PLUGIN_DATA="$PLUGIN_DIR/data" + +mkdir -p "$PLUGIN_DIR" "$PLUGIN_HOOKS" "$PLUGIN_CONFIGS" "$PLUGIN_DATA" 2>/dev/null + +# Plugin state +declare -A LOADED_PLUGINS +declare -A PLUGIN_METADATA +declare -A PLUGIN_HOOKS_REGISTERED + +# Available hook points +HOOK_POINTS=( + "on_dashboard_load" + "on_dashboard_render" + "on_dashboard_close" + "on_data_refresh" + "on_key_press" + "on_notification" + "on_error" + "on_metric_collect" + "on_theme_change" + "on_export" +) + +#─────────────────────────────────────────────────────────────────────────────── +# PLUGIN MANIFEST +#─────────────────────────────────────────────────────────────────────────────── + +# Create plugin manifest template +create_plugin_template() { + local plugin_name="$1" + local plugin_dir="$PLUGIN_DIR/$plugin_name" + + mkdir -p "$plugin_dir" + + # Create manifest + cat > "$plugin_dir/manifest.json" << EOF +{ + "name": "$plugin_name", + "version": "1.0.0", + "description": "A BlackRoad Dashboard Plugin", + "author": "Anonymous", + "license": "MIT", + "homepage": "", + "repository": "", + "main": "main.sh", + "hooks": [], + "dependencies": [], + "config": { + "enabled": true, + "settings": {} + }, + "permissions": [ + "read_metrics", + "notifications" + ] +} +EOF + + # Create main script + cat > "$plugin_dir/main.sh" << 'EOF' +#!/bin/bash +# Plugin: ${PLUGIN_NAME} +# Main entry point + +# Plugin initialization +plugin_init() { + log_debug "Plugin ${PLUGIN_NAME} initialized" +} + +# Plugin cleanup +plugin_cleanup() { + log_debug "Plugin ${PLUGIN_NAME} cleanup" +} + +# Hook handlers (implement as needed) +on_dashboard_load() { + : # Called when dashboard loads +} + +on_dashboard_render() { + : # Called on each render +} + +on_data_refresh() { + : # Called when data refreshes +} + +# Plugin render function (for UI plugins) +plugin_render() { + : # Custom render logic +} + +# Plugin configuration +plugin_configure() { + : # Configuration UI +} + +# Register this plugin +plugin_init +EOF + + chmod +x "$plugin_dir/main.sh" + + printf "Plugin template created: %s\n" "$plugin_dir" +} + +#─────────────────────────────────────────────────────────────────────────────── +# PLUGIN LOADING +#─────────────────────────────────────────────────────────────────────────────── + +# Parse plugin manifest +parse_manifest() { + local manifest_file="$1" + + [[ ! -f "$manifest_file" ]] && return 1 + + if command -v jq &>/dev/null; then + cat "$manifest_file" + else + cat "$manifest_file" + fi +} + +# Validate plugin +validate_plugin() { + local plugin_dir="$1" + local manifest="$plugin_dir/manifest.json" + local main_script="$plugin_dir/main.sh" + + [[ ! -d "$plugin_dir" ]] && return 1 + [[ ! -f "$manifest" ]] && return 1 + [[ ! -f "$main_script" ]] && return 1 + + # Check manifest structure + if command -v jq &>/dev/null; then + local name=$(jq -r '.name // empty' "$manifest" 2>/dev/null) + [[ -z "$name" ]] && return 1 + fi + + return 0 +} + +# Load a single plugin +load_plugin() { + local plugin_name="$1" + local plugin_dir="$PLUGIN_DIR/$plugin_name" + + # Validate + if ! validate_plugin "$plugin_dir"; then + log_error "Invalid plugin: $plugin_name" + return 1 + fi + + local manifest="$plugin_dir/manifest.json" + local main_script="$plugin_dir/main.sh" + + # Check if enabled + local enabled=true + if command -v jq &>/dev/null; then + enabled=$(jq -r '.config.enabled // true' "$manifest" 2>/dev/null) + fi + + [[ "$enabled" == "false" ]] && return 0 + + # Store metadata + if command -v jq &>/dev/null; then + PLUGIN_METADATA[$plugin_name]=$(jq -c '.' "$manifest" 2>/dev/null) + fi + + # Export plugin context + export PLUGIN_NAME="$plugin_name" + export PLUGIN_DIR="$plugin_dir" + export PLUGIN_CONFIG="$PLUGIN_CONFIGS/$plugin_name.json" + export PLUGIN_DATA_DIR="$PLUGIN_DATA/$plugin_name" + + mkdir -p "$PLUGIN_DATA_DIR" 2>/dev/null + + # Source the plugin + if source "$main_script" 2>/dev/null; then + LOADED_PLUGINS[$plugin_name]="loaded" + + # Register hooks + if command -v jq &>/dev/null; then + local hooks=$(jq -r '.hooks[]?' "$manifest" 2>/dev/null) + for hook in $hooks; do + register_hook "$hook" "$plugin_name" + done + fi + + log_info "Plugin loaded: $plugin_name" + return 0 + else + log_error "Failed to load plugin: $plugin_name" + return 1 + fi +} + +# Load all plugins +load_all_plugins() { + for plugin_dir in "$PLUGIN_DIR"/*/; do + [[ -d "$plugin_dir" ]] || continue + local plugin_name=$(basename "$plugin_dir") + load_plugin "$plugin_name" + done +} + +# Unload a plugin +unload_plugin() { + local plugin_name="$1" + + if [[ "${LOADED_PLUGINS[$plugin_name]}" == "loaded" ]]; then + # Call cleanup if exists + if type -t "plugin_cleanup" &>/dev/null; then + export PLUGIN_NAME="$plugin_name" + plugin_cleanup + fi + + unset LOADED_PLUGINS[$plugin_name] + log_info "Plugin unloaded: $plugin_name" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# HOOK SYSTEM +#─────────────────────────────────────────────────────────────────────────────── + +# Register a hook +register_hook() { + local hook_name="$1" + local plugin_name="$2" + + local current="${PLUGIN_HOOKS_REGISTERED[$hook_name]:-}" + if [[ -z "$current" ]]; then + PLUGIN_HOOKS_REGISTERED[$hook_name]="$plugin_name" + else + PLUGIN_HOOKS_REGISTERED[$hook_name]="$current,$plugin_name" + fi +} + +# Trigger a hook +trigger_hook() { + local hook_name="$1" + shift + local args=("$@") + + local plugins="${PLUGIN_HOOKS_REGISTERED[$hook_name]:-}" + [[ -z "$plugins" ]] && return 0 + + IFS=',' read -ra plugin_list <<< "$plugins" + + for plugin_name in "${plugin_list[@]}"; do + if [[ "${LOADED_PLUGINS[$plugin_name]}" == "loaded" ]]; then + export PLUGIN_NAME="$plugin_name" + export PLUGIN_DIR="$PLUGIN_DIR/$plugin_name" + + # Call the hook function if it exists + if type -t "$hook_name" &>/dev/null; then + "$hook_name" "${args[@]}" 2>/dev/null || true + fi + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# PLUGIN API +#─────────────────────────────────────────────────────────────────────────────── + +# Get plugin config value +plugin_get_config() { + local key="$1" + local default="${2:-}" + + [[ -z "$PLUGIN_CONFIG" ]] && echo "$default" && return + + if [[ -f "$PLUGIN_CONFIG" ]] && command -v jq &>/dev/null; then + local value=$(jq -r ".$key // empty" "$PLUGIN_CONFIG" 2>/dev/null) + [[ -n "$value" ]] && echo "$value" || echo "$default" + else + echo "$default" + fi +} + +# Set plugin config value +plugin_set_config() { + local key="$1" + local value="$2" + + [[ -z "$PLUGIN_CONFIG" ]] && return 1 + + if command -v jq &>/dev/null; then + local current="{}" + [[ -f "$PLUGIN_CONFIG" ]] && current=$(cat "$PLUGIN_CONFIG") + + echo "$current" | jq ".$key = \"$value\"" > "$PLUGIN_CONFIG" + fi +} + +# Store plugin data +plugin_store() { + local key="$1" + local value="$2" + + [[ -z "$PLUGIN_DATA_DIR" ]] && return 1 + + echo "$value" > "$PLUGIN_DATA_DIR/$key.dat" +} + +# Retrieve plugin data +plugin_retrieve() { + local key="$1" + local default="${2:-}" + + [[ -z "$PLUGIN_DATA_DIR" ]] && echo "$default" && return + + local file="$PLUGIN_DATA_DIR/$key.dat" + [[ -f "$file" ]] && cat "$file" || echo "$default" +} + +# Plugin notification +plugin_notify() { + local level="$1" + local title="$2" + local message="$3" + + # Use the notification system if available + if [[ -f "$SCRIPT_DIR/notification-system.sh" ]]; then + source "$SCRIPT_DIR/notification-system.sh" + notify "$level" "[${PLUGIN_NAME}] $title" "$message" + else + log_info "[$PLUGIN_NAME] $title: $message" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# PLUGIN MANAGER UI +#─────────────────────────────────────────────────────────────────────────────── + +plugin_manager() { + clear_screen + cursor_hide + + while true; do + cursor_to 1 1 + + printf "${BR_PURPLE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🔌 BLACKROAD PLUGIN MANAGER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # List installed plugins + printf "${BOLD}Installed Plugins:${RST}\n\n" + + local idx=1 + for plugin_dir in "$PLUGIN_DIR"/*/; do + [[ -d "$plugin_dir" ]] || continue + local plugin_name=$(basename "$plugin_dir") + local manifest="$plugin_dir/manifest.json" + + local status="${LOADED_PLUGINS[$plugin_name]:-not loaded}" + local status_color="$TEXT_MUTED" + [[ "$status" == "loaded" ]] && status_color="$BR_GREEN" + + local version="unknown" + local description="" + + if [[ -f "$manifest" ]] && command -v jq &>/dev/null; then + version=$(jq -r '.version // "1.0.0"' "$manifest" 2>/dev/null) + description=$(jq -r '.description // ""' "$manifest" 2>/dev/null) + fi + + printf " ${BR_CYAN}%d.${RST} ${BOLD}%-20s${RST} ${TEXT_MUTED}v%s${RST} ${status_color}[%s]${RST}\n" \ + "$idx" "$plugin_name" "$version" "$status" + [[ -n "$description" ]] && printf " ${TEXT_SECONDARY}%s${RST}\n" "$description" + + ((idx++)) + done + + [[ $idx -eq 1 ]] && printf " ${TEXT_MUTED}No plugins installed.${RST}\n" + + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[n]ew plugin [l]oad all [u]nload all [r]efresh [q]uit${RST}\n" + + if read -rsn1 -t 5 key 2>/dev/null; then + case "$key" in + n|N) + printf "\n${BR_CYAN}Enter plugin name: ${RST}" + cursor_show + read -r new_name + cursor_hide + [[ -n "$new_name" ]] && create_plugin_template "$new_name" + sleep 2 + ;; + l|L) + load_all_plugins + sleep 1 + ;; + u|U) + for plugin in "${!LOADED_PLUGINS[@]}"; do + unload_plugin "$plugin" + done + sleep 1 + ;; + r|R) continue ;; + q|Q) break ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# BUILT-IN PLUGINS +#─────────────────────────────────────────────────────────────────────────────── + +# Create sample plugins +create_sample_plugins() { + # Weather plugin + local weather_dir="$PLUGIN_DIR/weather-widget" + mkdir -p "$weather_dir" + + cat > "$weather_dir/manifest.json" << 'EOF' +{ + "name": "weather-widget", + "version": "1.0.0", + "description": "Display weather information in dashboard", + "author": "BlackRoad Team", + "hooks": ["on_dashboard_render"], + "config": {"enabled": true, "city": "San Francisco"} +} +EOF + + cat > "$weather_dir/main.sh" << 'EOF' +#!/bin/bash +plugin_init() { log_debug "Weather widget initialized"; } + +on_dashboard_render() { + local city=$(plugin_get_config "city" "San Francisco") + printf "${BR_YELLOW}☀${RST} Weather: %s - Sunny 72°F\n" "$city" +} +EOF + + # System monitor plugin + local sysmon_dir="$PLUGIN_DIR/system-monitor" + mkdir -p "$sysmon_dir" + + cat > "$sysmon_dir/manifest.json" << 'EOF' +{ + "name": "system-monitor", + "version": "1.0.0", + "description": "Enhanced system monitoring with alerts", + "author": "BlackRoad Team", + "hooks": ["on_metric_collect"], + "config": {"enabled": true, "cpu_threshold": 90, "mem_threshold": 85} +} +EOF + + cat > "$sysmon_dir/main.sh" << 'EOF' +#!/bin/bash +plugin_init() { log_debug "System monitor initialized"; } + +on_metric_collect() { + local cpu="$1" + local mem="$2" + local cpu_thresh=$(plugin_get_config "cpu_threshold" "90") + local mem_thresh=$(plugin_get_config "mem_threshold" "85") + + [[ $cpu -gt $cpu_thresh ]] && plugin_notify "warning" "High CPU" "CPU at ${cpu}%" + [[ $mem -gt $mem_thresh ]] && plugin_notify "warning" "High Memory" "Memory at ${mem}%" +} +EOF + + chmod +x "$weather_dir/main.sh" "$sysmon_dir/main.sh" + log_info "Sample plugins created" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-manager}" in + manager) plugin_manager ;; + load) load_plugin "$2" ;; + loadall) load_all_plugins ;; + unload) unload_plugin "$2" ;; + create) create_plugin_template "$2" ;; + samples) create_sample_plugins ;; + list) + for plugin in "$PLUGIN_DIR"/*/; do + [[ -d "$plugin" ]] && basename "$plugin" + done + ;; + *) + printf "Usage: %s [manager|load|loadall|unload|create|samples|list]\n" "$0" + ;; + esac +fi diff --git a/pomodoro-timer.sh b/pomodoro-timer.sh new file mode 100644 index 0000000..06ac62f --- /dev/null +++ b/pomodoro-timer.sh @@ -0,0 +1,483 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██████╗ ███╗ ███╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ +# ██╔══██╗██╔═══██╗████╗ ████║██╔═══██╗██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗ +# ██████╔╝██║ ██║██╔████╔██║██║ ██║██║ ██║██║ ██║██████╔╝██║ ██║ +# ██╔═══╝ ██║ ██║██║╚██╔╝██║██║ ██║██║ ██║██║ ██║██╔══██╗██║ ██║ +# ██║ ╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██████╔╝╚██████╔╝██║ ██║╚██████╔╝ +# ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD POMODORO TIMER v3.0 +# Productivity Timer with Statistics & Focus Tracking +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +DATA_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/pomodoro" +STATS_FILE="$DATA_DIR/stats.json" +HISTORY_FILE="$DATA_DIR/history.log" + +mkdir -p "$DATA_DIR" 2>/dev/null + +# Timer settings (in minutes) +WORK_DURATION=25 +SHORT_BREAK=5 +LONG_BREAK=15 +POMODOROS_UNTIL_LONG=4 + +# State +CURRENT_MODE="work" # work, short_break, long_break +POMODORO_COUNT=0 +TOTAL_FOCUS_TODAY=0 +IS_PAUSED=false +CURRENT_TASK="" + +#─────────────────────────────────────────────────────────────────────────────── +# TIMER DISPLAY +#─────────────────────────────────────────────────────────────────────────────── + +render_big_time() { + local minutes="$1" + local seconds="$2" + + local time_str=$(printf "%02d:%02d" "$minutes" "$seconds") + + # Big digits + declare -A DIGITS + DIGITS[0]="█████ +█ █ +█ █ +█ █ +█████" + DIGITS[1]=" █ + █ + █ + █ + █ " + DIGITS[2]="█████ + █ +█████ +█ +█████" + DIGITS[3]="█████ + █ +█████ + █ +█████" + DIGITS[4]="█ █ +█ █ +█████ + █ + █" + DIGITS[5]="█████ +█ +█████ + █ +█████" + DIGITS[6]="█████ +█ +█████ +█ █ +█████" + DIGITS[7]="█████ + █ + █ + █ + █ " + DIGITS[8]="█████ +█ █ +█████ +█ █ +█████" + DIGITS[9]="█████ +█ █ +█████ + █ +█████" + DIGITS[:]=" + █ + + █ + " + + local color="\033[38;5;46m" + [[ "$CURRENT_MODE" == "short_break" ]] && color="\033[38;5;39m" + [[ "$CURRENT_MODE" == "long_break" ]] && color="\033[38;5;201m" + [[ "$IS_PAUSED" == "true" ]] && color="\033[38;5;226m" + + # Render each row + for row in 0 1 2 3 4; do + printf " " + for ((i=0; i<${#time_str}; i++)); do + local char="${time_str:$i:1}" + local digit_art="${DIGITS[$char]}" + local line=$(echo "$digit_art" | sed -n "$((row+1))p") + printf "%s%s\033[0m " "$color" "$line" + done + printf "\n" + done +} + +render_progress_bar() { + local current="$1" + local total="$2" + local width=50 + + local filled=$((current * width / total)) + local empty=$((width - filled)) + + local color="\033[38;5;46m" + [[ "$CURRENT_MODE" == "short_break" ]] && color="\033[38;5;39m" + [[ "$CURRENT_MODE" == "long_break" ]] && color="\033[38;5;201m" + + printf " [" + printf "%s" "$color" + printf "%0.s█" $(seq 1 $filled 2>/dev/null) || true + printf "\033[38;5;240m" + printf "%0.s░" $(seq 1 $empty 2>/dev/null) || true + printf "\033[0m]\n" +} + +render_tomatoes() { + local count="$1" + local max="${2:-8}" + + printf " " + for ((i=1; i<=max; i++)); do + if [[ $i -le $count ]]; then + printf "\033[38;5;196m🍅\033[0m" + else + printf "\033[38;5;240m○ \033[0m" + fi + done + printf "\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# NOTIFICATIONS +#─────────────────────────────────────────────────────────────────────────────── + +play_notification() { + local type="$1" + + # Try different notification methods + if command -v notify-send &>/dev/null; then + case "$type" in + work_done) + notify-send "Pomodoro Complete!" "Time for a break 🎉" -i dialog-information + ;; + break_done) + notify-send "Break Over!" "Ready to focus again? 💪" -i dialog-information + ;; + esac + fi + + # Terminal bell + printf "\a" + + # Sound (if available) + if command -v paplay &>/dev/null; then + paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null & + elif command -v afplay &>/dev/null; then + afplay /System/Library/Sounds/Glass.aiff 2>/dev/null & + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# STATISTICS +#─────────────────────────────────────────────────────────────────────────────── + +init_stats() { + if [[ ! -f "$STATS_FILE" ]]; then + echo '{"total_pomodoros":0,"total_focus_minutes":0,"daily_stats":{}}' > "$STATS_FILE" + fi +} + +update_stats() { + local pomodoros="$1" + local focus_minutes="$2" + + init_stats + + if command -v jq &>/dev/null; then + local today=$(date +%Y-%m-%d) + local updated=$(jq --arg date "$today" \ + --argjson poms "$pomodoros" \ + --argjson mins "$focus_minutes" \ + '.total_pomodoros += $poms | + .total_focus_minutes += $mins | + .daily_stats[$date].pomodoros = ((.daily_stats[$date].pomodoros // 0) + $poms) | + .daily_stats[$date].minutes = ((.daily_stats[$date].minutes // 0) + $mins)' \ + "$STATS_FILE") + echo "$updated" > "$STATS_FILE" + fi +} + +get_today_stats() { + init_stats + + if command -v jq &>/dev/null; then + local today=$(date +%Y-%m-%d) + local poms=$(jq -r ".daily_stats[\"$today\"].pomodoros // 0" "$STATS_FILE") + local mins=$(jq -r ".daily_stats[\"$today\"].minutes // 0" "$STATS_FILE") + echo "$poms|$mins" + else + echo "0|0" + fi +} + +get_total_stats() { + init_stats + + if command -v jq &>/dev/null; then + local poms=$(jq -r '.total_pomodoros // 0' "$STATS_FILE") + local mins=$(jq -r '.total_focus_minutes // 0' "$STATS_FILE") + echo "$poms|$mins" + else + echo "0|0" + fi +} + +log_session() { + local duration="$1" + local mode="$2" + local task="${3:-}" + + echo "$(date -Iseconds)|$mode|$duration|$task" >> "$HISTORY_FILE" +} + +render_stats() { + IFS='|' read -r today_poms today_mins <<< "$(get_today_stats)" + IFS='|' read -r total_poms total_mins <<< "$(get_total_stats)" + + local today_hours=$((today_mins / 60)) + local today_remaining=$((today_mins % 60)) + local total_hours=$((total_mins / 60)) + + printf "\n\033[1mStatistics\033[0m\n\n" + printf " \033[38;5;226mToday:\033[0m %d pomodoros, %dh %dm focus time\n" "$today_poms" "$today_hours" "$today_remaining" + printf " \033[38;5;51mAll Time:\033[0m %d pomodoros, %d hours total\n" "$total_poms" "$total_hours" +} + +#─────────────────────────────────────────────────────────────────────────────── +# TIMER LOGIC +#─────────────────────────────────────────────────────────────────────────────── + +run_timer() { + local duration_minutes="$1" + local duration_seconds=$((duration_minutes * 60)) + local start_time=$(date +%s) + local paused_time=0 + + clear + tput civis + + trap 'tput cnorm; exit 0' INT TERM + + while true; do + local current_time=$(date +%s) + local elapsed=$((current_time - start_time - paused_time)) + local remaining=$((duration_seconds - elapsed)) + + if [[ $remaining -le 0 ]]; then + # Timer complete + play_notification "${CURRENT_MODE}_done" + + if [[ "$CURRENT_MODE" == "work" ]]; then + ((POMODORO_COUNT++)) + ((TOTAL_FOCUS_TODAY += duration_minutes)) + update_stats 1 "$duration_minutes" + log_session "$duration_minutes" "work" "$CURRENT_TASK" + fi + + return 0 + fi + + local mins=$((remaining / 60)) + local secs=$((remaining % 60)) + + # Render + tput cup 0 0 + + printf "\033[1;38;5;196m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🍅 POMODORO TIMER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m\n" + + # Mode indicator + local mode_text="" + case "$CURRENT_MODE" in + work) mode_text="\033[38;5;196m🔥 FOCUS TIME\033[0m" ;; + short_break) mode_text="\033[38;5;39m☕ SHORT BREAK\033[0m" ;; + long_break) mode_text="\033[38;5;201m🌴 LONG BREAK\033[0m" ;; + esac + + printf " %s" "$mode_text" + [[ "$IS_PAUSED" == "true" ]] && printf " \033[38;5;226m⏸ PAUSED\033[0m" + printf "\n\n" + + # Task name + [[ -n "$CURRENT_TASK" ]] && printf " \033[38;5;245mTask: %s\033[0m\n\n" "$CURRENT_TASK" + + # Big time display + render_big_time "$mins" "$secs" + printf "\n" + + # Progress bar + local progress=$((elapsed * 100 / duration_seconds)) + render_progress_bar "$elapsed" "$duration_seconds" + printf "\n" + + # Tomatoes + render_tomatoes "$POMODORO_COUNT" + printf "\n" + + # Stats + render_stats + + # Controls + printf "\n\033[38;5;240m───────────────────────────────────────────────────────────────────────────────\033[0m\n" + printf " \033[38;5;245m[Space] Pause [S] Skip [R] Reset [Q] Quit\033[0m\n" + + # Handle input + if [[ "$IS_PAUSED" == "true" ]]; then + if read -rsn1 key 2>/dev/null; then + case "$key" in + ' ') + IS_PAUSED=false + paused_time=$((paused_time + $(date +%s) - pause_start)) + ;; + s|S) return 1 ;; # Skip + r|R) return 2 ;; # Reset + q|Q) tput cnorm; exit 0 ;; + esac + fi + else + if read -rsn1 -t 1 key 2>/dev/null; then + case "$key" in + ' ') + IS_PAUSED=true + pause_start=$(date +%s) + ;; + s|S) return 1 ;; + r|R) return 2 ;; + q|Q) tput cnorm; exit 0 ;; + esac + fi + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN LOOP +#─────────────────────────────────────────────────────────────────────────────── + +pomodoro_cycle() { + while true; do + # Work session + CURRENT_MODE="work" + run_timer "$WORK_DURATION" + local result=$? + + [[ $result -eq 2 ]] && continue # Reset + + # Break + if [[ $((POMODORO_COUNT % POMODOROS_UNTIL_LONG)) -eq 0 && $POMODORO_COUNT -gt 0 ]]; then + CURRENT_MODE="long_break" + run_timer "$LONG_BREAK" + else + CURRENT_MODE="short_break" + run_timer "$SHORT_BREAK" + fi + done +} + +start_pomodoro() { + clear + + printf "\033[1;38;5;196m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🍅 POMODORO TIMER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m\n" + + printf " \033[1mPomodoro Settings\033[0m\n\n" + printf " Work: \033[38;5;196m%d minutes\033[0m\n" "$WORK_DURATION" + printf " Short Break: \033[38;5;39m%d minutes\033[0m\n" "$SHORT_BREAK" + printf " Long Break: \033[38;5;201m%d minutes\033[0m (every %d pomodoros)\n\n" "$LONG_BREAK" "$POMODOROS_UNTIL_LONG" + + printf " \033[38;5;51mWhat are you working on? (optional): \033[0m" + read -r CURRENT_TASK + + printf "\n \033[38;5;245mPress any key to start...\033[0m" + read -rsn1 + + pomodoro_cycle +} + +#─────────────────────────────────────────────────────────────────────────────── +# SETTINGS +#─────────────────────────────────────────────────────────────────────────────── + +settings_menu() { + while true; do + clear + printf "\033[1mPomodoro Settings\033[0m\n\n" + + printf " 1. Work duration: %d minutes\n" "$WORK_DURATION" + printf " 2. Short break: %d minutes\n" "$SHORT_BREAK" + printf " 3. Long break: %d minutes\n" "$LONG_BREAK" + printf " 4. Pomodoros until long: %d\n" "$POMODOROS_UNTIL_LONG" + printf "\n S. Save & Start\n" + printf " Q. Back\n\n" + printf " Choice: " + + read -rsn1 choice + + case "$choice" in + 1) + printf "\n Work duration (minutes): " + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && WORK_DURATION=$val + ;; + 2) + printf "\n Short break (minutes): " + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && SHORT_BREAK=$val + ;; + 3) + printf "\n Long break (minutes): " + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && LONG_BREAK=$val + ;; + 4) + printf "\n Pomodoros until long break: " + read -r val + [[ "$val" =~ ^[0-9]+$ ]] && POMODOROS_UNTIL_LONG=$val + ;; + s|S) start_pomodoro; break ;; + q|Q) break ;; + esac + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-start}" in + start) start_pomodoro ;; + settings) settings_menu ;; + stats) render_stats ;; + *) + printf "Usage: %s [start|settings|stats]\n" "$0" + ;; + esac +fi diff --git a/process-manager.sh b/process-manager.sh new file mode 100644 index 0000000..a49f5e8 --- /dev/null +++ b/process-manager.sh @@ -0,0 +1,486 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ██████╗ ██████╗ ██████╗███████╗███████╗███████╗ +# ██╔══██╗██╔══██╗██╔═══██╗██╔════╝██╔════╝██╔════╝██╔════╝ +# ██████╔╝██████╔╝██║ ██║██║ █████╗ ███████╗███████╗ +# ██╔═══╝ ██╔══██╗██║ ██║██║ ██╔══╝ ╚════██║╚════██║ +# ██║ ██║ ██║╚██████╔╝╚██████╗███████╗███████║███████║ +# ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚══════╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD PROCESS MANAGER v3.0 +# Interactive htop-like Process Viewer & Manager +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +SORT_BY="cpu" # cpu, mem, pid, name +SORT_ORDER="desc" +FILTER="" +SELECTED_IDX=0 +SCROLL_OFFSET=0 +SHOW_THREADS=false +SHOW_TREE=false +MAX_DISPLAY=20 + +#─────────────────────────────────────────────────────────────────────────────── +# SYSTEM METRICS +#─────────────────────────────────────────────────────────────────────────────── + +get_cpu_usage() { + local idle=$(grep 'cpu ' /proc/stat | awk '{print $5}') + local total=$(grep 'cpu ' /proc/stat | awk '{print $2+$3+$4+$5+$6+$7+$8}') + + if [[ -f /tmp/cpu_prev ]]; then + local prev_idle=$(cut -d' ' -f1 /tmp/cpu_prev) + local prev_total=$(cut -d' ' -f2 /tmp/cpu_prev) + + local diff_idle=$((idle - prev_idle)) + local diff_total=$((total - prev_total)) + + if [[ $diff_total -gt 0 ]]; then + echo "scale=1; 100 * (1 - $diff_idle / $diff_total)" | bc -l 2>/dev/null || echo "0" + else + echo "0" + fi + else + echo "0" + fi + + echo "$idle $total" > /tmp/cpu_prev +} + +get_memory_info() { + local total=$(grep MemTotal /proc/meminfo | awk '{print $2}') + local available=$(grep MemAvailable /proc/meminfo | awk '{print $2}') + local used=$((total - available)) + local pct=$((used * 100 / total)) + + echo "$used|$total|$pct" +} + +get_swap_info() { + local total=$(grep SwapTotal /proc/meminfo | awk '{print $2}') + local free=$(grep SwapFree /proc/meminfo | awk '{print $2}') + local used=$((total - free)) + local pct=0 + [[ $total -gt 0 ]] && pct=$((used * 100 / total)) + + echo "$used|$total|$pct" +} + +get_load_average() { + cat /proc/loadavg | awk '{print $1, $2, $3}' +} + +get_uptime() { + uptime -p 2>/dev/null || uptime | grep -oP 'up \K[^,]+' +} + +get_cpu_count() { + nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo +} + +#─────────────────────────────────────────────────────────────────────────────── +# PROCESS LISTING +#─────────────────────────────────────────────────────────────────────────────── + +get_processes() { + local sort_field="" + local sort_flags="-rn" + + case "$SORT_BY" in + cpu) sort_field=3 ;; + mem) sort_field=4 ;; + pid) sort_field=1; sort_flags="-n" ;; + name) sort_field=5; sort_flags="" ;; + esac + + [[ "$SORT_ORDER" == "asc" ]] && sort_flags="${sort_flags//-r/}" + + local thread_flag="" + [[ "$SHOW_THREADS" == "true" ]] && thread_flag="-L" + + ps aux $thread_flag --no-headers 2>/dev/null | \ + awk '{printf "%s|%s|%.1f|%.1f|%s|%s\n", $2, $1, $3, $4, $11, $0}' | \ + sort -t'|' -k${sort_field} $sort_flags | \ + if [[ -n "$FILTER" ]]; then + grep -i "$FILTER" + else + cat + fi +} + +get_process_count() { + ps aux --no-headers 2>/dev/null | wc -l +} + +get_thread_count() { + ps -eLf --no-headers 2>/dev/null | wc -l +} + +get_running_count() { + ps aux --no-headers 2>/dev/null | awk '$8 ~ /R/ {count++} END {print count+0}' +} + +#─────────────────────────────────────────────────────────────────────────────── +# PROCESS TREE +#─────────────────────────────────────────────────────────────────────────────── + +get_process_tree() { + if command -v pstree &>/dev/null; then + pstree -p -U 2>/dev/null | head -50 + else + ps auxf --no-headers 2>/dev/null | head -50 + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# PROCESS ACTIONS +#─────────────────────────────────────────────────────────────────────────────── + +kill_process() { + local pid="$1" + local signal="${2:-15}" + + kill -"$signal" "$pid" 2>/dev/null +} + +renice_process() { + local pid="$1" + local priority="$2" + + renice "$priority" -p "$pid" 2>/dev/null +} + +get_process_details() { + local pid="$1" + + local cmdline=$(cat /proc/"$pid"/cmdline 2>/dev/null | tr '\0' ' ') + local status=$(cat /proc/"$pid"/status 2>/dev/null) + local stat=$(cat /proc/"$pid"/stat 2>/dev/null) + local fd_count=$(ls /proc/"$pid"/fd 2>/dev/null | wc -l) + + local name=$(echo "$status" | grep ^Name: | awk '{print $2}') + local state=$(echo "$status" | grep ^State: | awk '{print $2}') + local ppid=$(echo "$status" | grep ^PPid: | awk '{print $2}') + local threads=$(echo "$status" | grep ^Threads: | awk '{print $2}') + local vm_rss=$(echo "$status" | grep ^VmRSS: | awk '{print $2, $3}') + local vm_size=$(echo "$status" | grep ^VmSize: | awk '{print $2, $3}') + local uid=$(echo "$status" | grep ^Uid: | awk '{print $2}') + + local user=$(getent passwd "$uid" 2>/dev/null | cut -d: -f1) + [[ -z "$user" ]] && user="$uid" + + printf "PID: %s\n" "$pid" + printf "Name: %s\n" "$name" + printf "State: %s\n" "$state" + printf "User: %s\n" "$user" + printf "PPID: %s\n" "$ppid" + printf "Threads: %s\n" "$threads" + printf "Memory (RSS): %s\n" "$vm_rss" + printf "Virtual Size: %s\n" "$vm_size" + printf "File Descriptors: %s\n" "$fd_count" + printf "Command: %s\n" "${cmdline:0:60}" +} + +#─────────────────────────────────────────────────────────────────────────────── +# RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +render_meter() { + local value="$1" + local max="${2:-100}" + local width="${3:-30}" + local label="${4:-}" + local color="${5:-}" + + local filled=$((value * width / max)) + [[ $filled -gt $width ]] && filled=$width + local empty=$((width - filled)) + + [[ -z "$color" ]] && { + if [[ $value -gt 80 ]]; then + color="\033[38;5;196m" + elif [[ $value -gt 50 ]]; then + color="\033[38;5;226m" + else + color="\033[38;5;46m" + fi + } + + printf "%s" "$color" + printf "%0.s|" $(seq 1 $filled 2>/dev/null) || true + printf "\033[38;5;240m" + printf "%0.s|" $(seq 1 $empty 2>/dev/null) || true + printf "\033[0m" + + [[ -n "$label" ]] && printf " %s" "$label" +} + +render_cpu_meters() { + local cpu_count=$(get_cpu_count) + local overall=$(get_cpu_usage) + + printf " \033[1mCPU\033[0m [" + render_meter "${overall%.*}" 100 20 + printf "] %.1f%%\n" "$overall" + + # Per-CPU if available + if [[ -f /proc/stat ]] && [[ $cpu_count -le 8 ]]; then + for ((i=0; i<cpu_count && i<4; i++)); do + local cpu_line=$(grep "^cpu$i " /proc/stat) + local user=$(echo "$cpu_line" | awk '{print $2}') + local system=$(echo "$cpu_line" | awk '{print $4}') + local idle=$(echo "$cpu_line" | awk '{print $5}') + local total=$((user + system + idle)) + local usage=0 + [[ $total -gt 0 ]] && usage=$(( (user + system) * 100 / total )) + + printf " \033[38;5;240mCPU%d\033[0m [" "$i" + render_meter "$usage" 100 15 + printf "] %3d%%\n" "$usage" + done + fi +} + +render_memory_meters() { + IFS='|' read -r mem_used mem_total mem_pct <<< "$(get_memory_info)" + IFS='|' read -r swap_used swap_total swap_pct <<< "$(get_swap_info)" + + local mem_gb=$(echo "scale=1; $mem_used / 1048576" | bc -l 2>/dev/null || echo "0") + local mem_total_gb=$(echo "scale=1; $mem_total / 1048576" | bc -l 2>/dev/null || echo "0") + + printf " \033[1mMem\033[0m [" + render_meter "$mem_pct" 100 20 + printf "] %.1fG/%.1fG\n" "$mem_gb" "$mem_total_gb" + + if [[ $swap_total -gt 0 ]]; then + local swap_gb=$(echo "scale=1; $swap_used / 1048576" | bc -l 2>/dev/null || echo "0") + local swap_total_gb=$(echo "scale=1; $swap_total / 1048576" | bc -l 2>/dev/null || echo "0") + + printf " \033[1mSwp\033[0m [" + render_meter "$swap_pct" 100 20 + printf "] %.1fG/%.1fG\n" "$swap_gb" "$swap_total_gb" + fi +} + +render_header() { + printf "\033[1;38;5;39m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ ⚡ BLACKROAD PROCESS MANAGER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m\n" +} + +render_system_info() { + render_cpu_meters + printf "\n" + render_memory_meters + printf "\n" + + local load=$(get_load_average) + local uptime=$(get_uptime) + local procs=$(get_process_count) + local threads=$(get_thread_count) + local running=$(get_running_count) + + printf " \033[1mLoad:\033[0m %s \033[1mUptime:\033[0m %s\n" "$load" "$uptime" + printf " \033[1mTasks:\033[0m %d total, \033[38;5;46m%d running\033[0m, %d threads\n" "$procs" "$running" "$threads" +} + +render_process_list() { + local -a processes + mapfile -t processes < <(get_processes) + + local total=${#processes[@]} + + # Adjust scroll + [[ $SELECTED_IDX -lt 0 ]] && SELECTED_IDX=0 + [[ $SELECTED_IDX -ge $total ]] && SELECTED_IDX=$((total - 1)) + [[ $SELECTED_IDX -lt $SCROLL_OFFSET ]] && SCROLL_OFFSET=$SELECTED_IDX + [[ $SELECTED_IDX -ge $((SCROLL_OFFSET + MAX_DISPLAY)) ]] && SCROLL_OFFSET=$((SELECTED_IDX - MAX_DISPLAY + 1)) + + printf "\n" + printf "\033[1;38;5;240m" + printf " %6s %8s %6s %6s %-40s\n" "PID" "USER" "CPU%" "MEM%" "COMMAND" + printf " ────────────────────────────────────────────────────────────────────────\033[0m\n" + + for ((i=SCROLL_OFFSET; i<SCROLL_OFFSET+MAX_DISPLAY && i<total; i++)); do + IFS='|' read -r pid user cpu mem cmd full <<< "${processes[$i]}" + + local line_color="" + local prefix=" " + + if [[ $i -eq $SELECTED_IDX ]]; then + line_color="\033[48;5;236m\033[1m" + prefix="\033[38;5;51m▶\033[0m${line_color} " + fi + + # Color CPU/MEM + local cpu_color="\033[38;5;46m" + [[ ${cpu%.*} -gt 50 ]] && cpu_color="\033[38;5;226m" + [[ ${cpu%.*} -gt 80 ]] && cpu_color="\033[38;5;196m" + + local mem_color="\033[38;5;39m" + [[ ${mem%.*} -gt 50 ]] && mem_color="\033[38;5;226m" + [[ ${mem%.*} -gt 80 ]] && mem_color="\033[38;5;196m" + + printf "%s%s%6s \033[38;5;243m%8s\033[0m%s %s%5.1f%% %s%5.1f%%\033[0m%s %-40s\033[0m\n" \ + "$line_color" "$prefix" "$pid" "${user:0:8}" "$line_color" "$cpu_color" "$cpu" "$mem_color" "$mem" "$line_color" "${cmd:0:40}" + done + + # Scroll indicator + if [[ $total -gt $MAX_DISPLAY ]]; then + printf "\n \033[38;5;240m[%d-%d of %d]\033[0m" "$((SCROLL_OFFSET + 1))" "$((SCROLL_OFFSET + MAX_DISPLAY))" "$total" + fi +} + +render_controls() { + printf "\n\n\033[38;5;240m───────────────────────────────────────────────────────────────────────────────\033[0m\n" + printf " \033[38;5;39m↑/↓\033[0m Navigate " + printf "\033[38;5;196mF9\033[0m Kill " + printf "\033[38;5;226mk\033[0m SIGKILL " + printf "\033[38;5;46ms\033[0m Sort " + printf "\033[38;5;201m/\033[0m Filter " + printf "\033[38;5;240mq\033[0m Quit\n" + + printf " Sort: \033[38;5;51m%s\033[0m (%s) " "$SORT_BY" "$SORT_ORDER" + [[ -n "$FILTER" ]] && printf "Filter: \033[38;5;201m%s\033[0m " "$FILTER" + [[ "$SHOW_TREE" == "true" ]] && printf "\033[38;5;46m[TREE]\033[0m " + [[ "$SHOW_THREADS" == "true" ]] && printf "\033[38;5;226m[THREADS]\033[0m" + printf "\n" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN LOOP +#─────────────────────────────────────────────────────────────────────────────── + +process_manager_loop() { + clear + tput civis + + trap 'tput cnorm; clear; exit 0' INT TERM + + while true; do + tput cup 0 0 + + if [[ "$SHOW_TREE" == "true" ]]; then + render_header + printf "\033[1mProcess Tree:\033[0m\n\n" + get_process_tree + printf "\n\n\033[38;5;240mPress 't' to exit tree view, 'q' to quit\033[0m\n" + else + render_header + render_system_info + render_process_list + render_controls + fi + + # Handle input + if read -rsn1 -t 1 key 2>/dev/null; then + case "$key" in + $'\x1b') + read -rsn2 -t 0.01 key2 + case "$key2" in + '[A') ((SELECTED_IDX--)) ;; # Up + '[B') ((SELECTED_IDX++)) ;; # Down + '[5') ((SCROLL_OFFSET -= MAX_DISPLAY)); ((SELECTED_IDX -= MAX_DISPLAY)) ;; # PgUp + '[6') ((SCROLL_OFFSET += MAX_DISPLAY)); ((SELECTED_IDX += MAX_DISPLAY)) ;; # PgDn + '[2') # F9 (some terminals) + read -rsn1 + # Get selected PID and kill + local -a procs + mapfile -t procs < <(get_processes) + IFS='|' read -r pid rest <<< "${procs[$SELECTED_IDX]}" + [[ -n "$pid" ]] && kill_process "$pid" 15 + ;; + esac + ;; + k|K) + local -a procs + mapfile -t procs < <(get_processes) + IFS='|' read -r pid rest <<< "${procs[$SELECTED_IDX]}" + [[ -n "$pid" ]] && kill_process "$pid" 9 + ;; + 9) # F9 alternative + local -a procs + mapfile -t procs < <(get_processes) + IFS='|' read -r pid rest <<< "${procs[$SELECTED_IDX]}" + [[ -n "$pid" ]] && kill_process "$pid" 15 + ;; + s|S) + case "$SORT_BY" in + cpu) SORT_BY="mem" ;; + mem) SORT_BY="pid" ;; + pid) SORT_BY="name" ;; + *) SORT_BY="cpu" ;; + esac + ;; + o|O) + [[ "$SORT_ORDER" == "desc" ]] && SORT_ORDER="asc" || SORT_ORDER="desc" + ;; + /) + tput cnorm + printf "\n Filter: " + read -r FILTER + tput civis + SELECTED_IDX=0 + SCROLL_OFFSET=0 + ;; + c|C) + FILTER="" + SELECTED_IDX=0 + SCROLL_OFFSET=0 + ;; + t|T) + [[ "$SHOW_TREE" == "true" ]] && SHOW_TREE=false || SHOW_TREE=true + clear + ;; + h|H) + [[ "$SHOW_THREADS" == "true" ]] && SHOW_THREADS=false || SHOW_THREADS=true + ;; + i|I) + # Show process details + local -a procs + mapfile -t procs < <(get_processes) + IFS='|' read -r pid rest <<< "${procs[$SELECTED_IDX]}" + if [[ -n "$pid" ]]; then + clear + printf "\033[1mProcess Details:\033[0m\n\n" + get_process_details "$pid" + printf "\n\nPress any key to continue..." + read -rsn1 + clear + fi + ;; + q|Q) + tput cnorm + clear + exit 0 + ;; + esac + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-run}" in + run) process_manager_loop ;; + list) get_processes | head -20 ;; + tree) get_process_tree ;; + kill) kill_process "$2" "${3:-15}" ;; + info) get_process_details "$2" ;; + *) + printf "Usage: %s [run|list|tree|kill|info]\n" "$0" + ;; + esac +fi diff --git a/security-ops.sh b/security-ops.sh new file mode 100644 index 0000000..50e7e07 --- /dev/null +++ b/security-ops.sh @@ -0,0 +1,527 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ███████╗███████╗ ██████╗██╗ ██╗██████╗ ██╗████████╗██╗ ██╗ +# ██╔════╝██╔════╝██╔════╝██║ ██║██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝ +# ███████╗█████╗ ██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ +# ╚════██║██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ +# ███████║███████╗╚██████╗╚██████╔╝██║ ██║██║ ██║ ██║ +# ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD SECURITY OPERATIONS CENTER v3.0 +# Threat Monitoring, Vulnerability Scanning, Security Alerts +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +SECURITY_HOME="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards}/security" +SECURITY_LOGS="$SECURITY_HOME/logs" +SECURITY_ALERTS="$SECURITY_HOME/alerts" +THREAT_DB="$SECURITY_HOME/threats.db" +mkdir -p "$SECURITY_HOME" "$SECURITY_LOGS" "$SECURITY_ALERTS" 2>/dev/null + +# Security levels +declare -A SEVERITY_LEVELS=( + [critical]=5 + [high]=4 + [medium]=3 + [low]=2 + [info]=1 +) + +# Threat tracking +declare -a ACTIVE_THREATS=() +declare -A THREAT_COUNTS +declare -a SECURITY_EVENTS=() + +# Scan results +declare -A PORT_SCAN_RESULTS +declare -A SSL_SCAN_RESULTS +declare -A VULN_SCAN_RESULTS + +#─────────────────────────────────────────────────────────────────────────────── +# THREAT DETECTION +#─────────────────────────────────────────────────────────────────────────────── + +# Log security event +log_security_event() { + local severity="$1" + local category="$2" + local message="$3" + local source="${4:-system}" + + local timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) + local event_id="evt_$(date +%s%N | tail -c 10)" + + local event=$(cat << EOF +{ + "id": "$event_id", + "timestamp": "$timestamp", + "severity": "$severity", + "category": "$category", + "message": "$message", + "source": "$source" +} +EOF +) + + SECURITY_EVENTS+=("$event") + echo "$event" >> "$SECURITY_LOGS/events_$(date +%Y%m%d).log" + + # Count by severity + THREAT_COUNTS[$severity]=$((${THREAT_COUNTS[$severity]:-0} + 1)) + + # Alert on critical/high + if [[ "${SEVERITY_LEVELS[$severity]:-0}" -ge 4 ]]; then + echo "$event" >> "$SECURITY_ALERTS/active.json" + + if [[ -f "$SCRIPT_DIR/notification-system.sh" ]]; then + source "$SCRIPT_DIR/notification-system.sh" + notify_security "Security Alert: $category" "$message" + fi + fi +} + +# Check for suspicious processes +check_suspicious_processes() { + local -a suspicious_patterns=( + "nc -l" + "ncat" + "/tmp/.*sh" + "base64.*decode" + "curl.*\|.*sh" + "wget.*\|.*sh" + ) + + local alerts=0 + + for pattern in "${suspicious_patterns[@]}"; do + if ps aux 2>/dev/null | grep -qE "$pattern"; then + log_security_event "high" "process" "Suspicious process detected: $pattern" + ((alerts++)) + fi + done + + echo "$alerts" +} + +# Check for unauthorized SSH sessions +check_ssh_sessions() { + local authorized_users=("root" "admin" "$USER") + local alerts=0 + + while IFS= read -r session; do + [[ -z "$session" ]] && continue + + local user=$(echo "$session" | awk '{print $1}') + local from=$(echo "$session" | awk '{print $NF}') + + local authorized=false + for auth_user in "${authorized_users[@]}"; do + [[ "$user" == "$auth_user" ]] && authorized=true && break + done + + if [[ "$authorized" != "true" ]]; then + log_security_event "high" "ssh" "Unauthorized SSH session: $user from $from" + ((alerts++)) + fi + done < <(who 2>/dev/null) + + echo "$alerts" +} + +# Check for failed login attempts +check_failed_logins() { + local threshold=5 + local alerts=0 + + if [[ -f /var/log/auth.log ]]; then + local failed_count=$(grep -c "Failed password" /var/log/auth.log 2>/dev/null || echo "0") + + if [[ $failed_count -gt $threshold ]]; then + log_security_event "medium" "auth" "Multiple failed login attempts: $failed_count" + ((alerts++)) + fi + fi + + echo "$alerts" +} + +# Check for open ports +check_open_ports() { + local known_ports=(22 80 443 8080) + local alerts=0 + + # Get listening ports + local open_ports + if command -v ss &>/dev/null; then + open_ports=$(ss -tuln 2>/dev/null | awk 'NR>1 {print $5}' | grep -oE '[0-9]+$' | sort -u) + elif command -v netstat &>/dev/null; then + open_ports=$(netstat -tuln 2>/dev/null | awk 'NR>2 {print $4}' | grep -oE '[0-9]+$' | sort -u) + fi + + for port in $open_ports; do + local known=false + for kp in "${known_ports[@]}"; do + [[ "$port" == "$kp" ]] && known=true && break + done + + if [[ "$known" != "true" && $port -lt 1024 ]]; then + log_security_event "low" "network" "Unknown privileged port open: $port" + ((alerts++)) + fi + + PORT_SCAN_RESULTS[$port]="open" + done + + echo "$alerts" +} + +# Check file permissions +check_file_permissions() { + local alerts=0 + + # Check for world-writable files in sensitive locations + local sensitive_dirs=("/etc" "/usr/bin" "/usr/sbin") + + for dir in "${sensitive_dirs[@]}"; do + [[ ! -d "$dir" ]] && continue + + local writable=$(find "$dir" -maxdepth 1 -perm -002 -type f 2>/dev/null | head -5) + if [[ -n "$writable" ]]; then + log_security_event "medium" "filesystem" "World-writable files in $dir" + ((alerts++)) + fi + done + + # Check SSH key permissions + if [[ -d "$HOME/.ssh" ]]; then + local bad_perms=$(find "$HOME/.ssh" -type f -perm /077 2>/dev/null) + if [[ -n "$bad_perms" ]]; then + log_security_event "high" "ssh" "Insecure SSH key permissions detected" + ((alerts++)) + fi + fi + + echo "$alerts" +} + +#─────────────────────────────────────────────────────────────────────────────── +# SSL/TLS SCANNING +#─────────────────────────────────────────────────────────────────────────────── + +# Check SSL certificate +check_ssl_certificate() { + local domain="$1" + local port="${2:-443}" + local warn_days="${3:-30}" + + local result + result=$(echo | timeout 10 openssl s_client -connect "${domain}:${port}" -servername "$domain" 2>/dev/null) + + if [[ -z "$result" ]]; then + SSL_SCAN_RESULTS[$domain]="connection_failed" + return 1 + fi + + # Get certificate details + local cert_info=$(echo "$result" | openssl x509 -noout -dates -subject 2>/dev/null) + local not_after=$(echo "$cert_info" | grep "notAfter" | cut -d= -f2) + + if [[ -n "$not_after" ]]; then + local expiry_epoch=$(date -d "$not_after" +%s 2>/dev/null || date -j -f "%b %d %H:%M:%S %Y %Z" "$not_after" +%s 2>/dev/null) + local now=$(date +%s) + local days_left=$(( (expiry_epoch - now) / 86400 )) + + if [[ $days_left -lt 0 ]]; then + SSL_SCAN_RESULTS[$domain]="expired" + log_security_event "critical" "ssl" "SSL certificate expired for $domain" + return 1 + elif [[ $days_left -lt $warn_days ]]; then + SSL_SCAN_RESULTS[$domain]="expiring:$days_left" + log_security_event "high" "ssl" "SSL certificate expiring in $days_left days for $domain" + return 0 + else + SSL_SCAN_RESULTS[$domain]="valid:$days_left" + return 0 + fi + fi + + SSL_SCAN_RESULTS[$domain]="unknown" + return 1 +} + +# Check SSL protocol versions +check_ssl_protocols() { + local domain="$1" + + local -a weak_protocols=("ssl2" "ssl3" "tls1" "tls1_1") + local alerts=0 + + for proto in "${weak_protocols[@]}"; do + if echo | timeout 5 openssl s_client -connect "${domain}:443" -"$proto" 2>/dev/null | grep -q "Cipher"; then + log_security_event "high" "ssl" "Weak protocol $proto enabled on $domain" + ((alerts++)) + fi + done + + echo "$alerts" +} + +#─────────────────────────────────────────────────────────────────────────────── +# VULNERABILITY ASSESSMENT +#─────────────────────────────────────────────────────────────────────────────── + +# Check for common vulnerabilities +run_vulnerability_scan() { + local target="${1:-localhost}" + local alerts=0 + + printf "${BR_CYAN}Running vulnerability scan on %s...${RST}\n" "$target" + + # Check for default credentials (simulation) + VULN_SCAN_RESULTS["default_creds"]="checking..." + + # Check for outdated software + if command -v apt &>/dev/null; then + local upgradable=$(apt list --upgradable 2>/dev/null | grep -c "upgradable") + if [[ $upgradable -gt 0 ]]; then + VULN_SCAN_RESULTS["outdated_packages"]="$upgradable packages" + log_security_event "medium" "vulnerability" "$upgradable packages need updating" + ((alerts++)) + fi + fi + + # Check for common misconfigurations + # 1. SSH root login + if grep -q "PermitRootLogin yes" /etc/ssh/sshd_config 2>/dev/null; then + VULN_SCAN_RESULTS["ssh_root_login"]="enabled" + log_security_event "high" "config" "SSH root login is enabled" + ((alerts++)) + fi + + # 2. Password authentication + if grep -q "PasswordAuthentication yes" /etc/ssh/sshd_config 2>/dev/null; then + VULN_SCAN_RESULTS["ssh_password_auth"]="enabled" + log_security_event "medium" "config" "SSH password authentication is enabled" + ((alerts++)) + fi + + # 3. Firewall status + if command -v ufw &>/dev/null; then + if ! ufw status 2>/dev/null | grep -q "active"; then + VULN_SCAN_RESULTS["firewall"]="disabled" + log_security_event "high" "firewall" "UFW firewall is not active" + ((alerts++)) + fi + fi + + echo "$alerts" +} + +#─────────────────────────────────────────────────────────────────────────────── +# SECURITY DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_security_dashboard() { + clear_screen + cursor_hide + + local frame=$(($(date +%s) % 2)) + local pulse="" + [[ $frame -eq 0 ]] && pulse="${BR_RED}${BLINK}" + + printf "${BR_RED}${BOLD}" + cat << 'BANNER' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ███████╗███████╗ ██████╗██╗ ██╗██████╗ ██╗████████╗██╗ ██╗ ║ +║ ██╔════╝██╔════╝██╔════╝██║ ██║██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝ ║ +║ ███████╗█████╗ ██║ ██║ ██║██████╔╝██║ ██║ ╚████╔╝ ║ +║ ╚════██║██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ ╚██╔╝ ║ +║ ███████║███████╗╚██████╗╚██████╔╝██║ ██║██║ ██║ ██║ ║ +║ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║ +║ O P E R A T I O N S C E N T E R ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +BANNER + printf "${RST}\n" + + # Threat Level Indicator + local critical=${THREAT_COUNTS[critical]:-0} + local high=${THREAT_COUNTS[high]:-0} + local medium=${THREAT_COUNTS[medium]:-0} + local low=${THREAT_COUNTS[low]:-0} + + local threat_level="LOW" + local threat_color="$BR_GREEN" + + if [[ $critical -gt 0 ]]; then + threat_level="CRITICAL" + threat_color="$BR_RED$BLINK" + elif [[ $high -gt 0 ]]; then + threat_level="HIGH" + threat_color="$BR_RED" + elif [[ $medium -gt 0 ]]; then + threat_level="MEDIUM" + threat_color="$BR_YELLOW" + fi + + printf " ${BOLD}THREAT LEVEL:${RST} ${threat_color}████ %s ████${RST}\n\n" "$threat_level" + + # Alert Summary + printf "${BR_ORANGE}┌─ ALERT SUMMARY ───────────────────────────────────────────────────────────┐${RST}\n" + printf "${BR_ORANGE}│${RST} ${BR_RED}Critical: %-5d${RST} ${BR_ORANGE}High: %-5d${RST} ${BR_YELLOW}Medium: %-5d${RST} ${BR_GREEN}Low: %-5d${RST} ${BR_ORANGE}│${RST}\n" \ + "$critical" "$high" "$medium" "$low" + printf "${BR_ORANGE}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # Run quick scans + printf "${BR_CYAN}┌─ REAL-TIME MONITORING ───────────────────────────────────────────────────┐${RST}\n" + + # Process check + printf "${BR_CYAN}│${RST} Suspicious Processes: " + local proc_alerts=$(check_suspicious_processes) + if [[ $proc_alerts -gt 0 ]]; then + printf "${BR_RED}%d detected${RST}\n" "$proc_alerts" + else + printf "${BR_GREEN}None${RST}\n" + fi + + # SSH sessions + printf "${BR_CYAN}│${RST} SSH Sessions: " + local ssh_alerts=$(check_ssh_sessions) + local active_sessions=$(who 2>/dev/null | wc -l) + if [[ $ssh_alerts -gt 0 ]]; then + printf "${BR_RED}%d unauthorized${RST} " "$ssh_alerts" + fi + printf "${TEXT_SECONDARY}(%d active)${RST}\n" "$active_sessions" + + # Open ports + printf "${BR_CYAN}│${RST} Open Ports: " + local port_alerts=$(check_open_ports) + printf "${TEXT_SECONDARY}%d ports${RST}" "${#PORT_SCAN_RESULTS[@]}" + [[ $port_alerts -gt 0 ]] && printf " ${BR_YELLOW}(%d unusual)${RST}" "$port_alerts" + printf "\n" + + # File permissions + printf "${BR_CYAN}│${RST} File Permissions: " + local perm_alerts=$(check_file_permissions) + if [[ $perm_alerts -gt 0 ]]; then + printf "${BR_YELLOW}%d issues${RST}\n" "$perm_alerts" + else + printf "${BR_GREEN}OK${RST}\n" + fi + + printf "${BR_CYAN}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # Recent Events + printf "${BR_PURPLE}┌─ RECENT SECURITY EVENTS ─────────────────────────────────────────────────┐${RST}\n" + + local event_count=0 + for event in "${SECURITY_EVENTS[@]: -5}"; do + [[ -z "$event" ]] && continue + ((event_count++)) + + local severity=$(echo "$event" | jq -r '.severity' 2>/dev/null) + local category=$(echo "$event" | jq -r '.category' 2>/dev/null) + local message=$(echo "$event" | jq -r '.message' 2>/dev/null | head -c 50) + local timestamp=$(echo "$event" | jq -r '.timestamp' 2>/dev/null | cut -dT -f2 | cut -d. -f1) + + local sev_color="$TEXT_MUTED" + case "$severity" in + critical) sev_color="$BR_RED$BOLD" ;; + high) sev_color="$BR_RED" ;; + medium) sev_color="$BR_YELLOW" ;; + low) sev_color="$BR_GREEN" ;; + esac + + printf "${BR_PURPLE}│${RST} ${TEXT_MUTED}%s${RST} ${sev_color}%-8s${RST} ${BR_CYAN}%-10s${RST} %s\n" \ + "$timestamp" "[$severity]" "[$category]" "$message" + done + + [[ $event_count -eq 0 ]] && printf "${BR_PURPLE}│${RST} ${TEXT_MUTED}No recent events${RST}\n" + + printf "${BR_PURPLE}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + # SSL Status + printf "${BR_GREEN}┌─ SSL/TLS STATUS ──────────────────────────────────────────────────────────┐${RST}\n" + + for domain in "${!SSL_SCAN_RESULTS[@]}"; do + local status="${SSL_SCAN_RESULTS[$domain]}" + local status_color="$BR_GREEN" + local status_icon="✓" + + case "$status" in + expired) status_color="$BR_RED"; status_icon="✗" ;; + expiring:*) status_color="$BR_YELLOW"; status_icon="⚠" ;; + valid:*) status_color="$BR_GREEN"; status_icon="✓" ;; + *) status_color="$TEXT_MUTED"; status_icon="?" ;; + esac + + printf "${BR_GREEN}│${RST} ${status_color}%s${RST} %-25s %s\n" "$status_icon" "$domain" "$status" + done + + [[ ${#SSL_SCAN_RESULTS[@]} -eq 0 ]] && printf "${BR_GREEN}│${RST} ${TEXT_MUTED}No SSL scans performed${RST}\n" + + printf "${BR_GREEN}└───────────────────────────────────────────────────────────────────────────┘${RST}\n\n" + + printf "${TEXT_MUTED}Last scan: $(date '+%H:%M:%S') │ [s]can [v]ulns [c]lear [q]uit${RST}\n" +} + +# Main security dashboard loop +security_dashboard_loop() { + # Initial scans + check_open_ports > /dev/null + check_ssl_certificate "github.com" > /dev/null + check_ssl_certificate "cloudflare.com" > /dev/null + + while true; do + render_security_dashboard + + if read -rsn1 -t 3 key 2>/dev/null; then + case "$key" in + s|S) + printf "\n${BR_CYAN}Running security scan...${RST}\n" + check_suspicious_processes > /dev/null + check_ssh_sessions > /dev/null + check_open_ports > /dev/null + check_file_permissions > /dev/null + sleep 2 + ;; + v|V) + printf "\n${BR_CYAN}Running vulnerability assessment...${RST}\n" + run_vulnerability_scan + sleep 3 + ;; + c|C) + SECURITY_EVENTS=() + THREAT_COUNTS=() + ;; + q|Q) + break + ;; + esac + fi + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) security_dashboard_loop ;; + scan) run_vulnerability_scan "$2" ;; + ssl) check_ssl_certificate "$2" "$3" ;; + ports) check_open_ports ;; + procs) check_suspicious_processes ;; + *) + printf "Usage: %s [dashboard|scan|ssl|ports|procs]\n" "$0" + ;; + esac +fi diff --git a/system-benchmark.sh b/system-benchmark.sh new file mode 100644 index 0000000..ceb9ebe --- /dev/null +++ b/system-benchmark.sh @@ -0,0 +1,654 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ ███████╗███╗ ██╗ ██████╗██╗ ██╗███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ +# ██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██║████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝ +# ██████╔╝█████╗ ██╔██╗ ██║██║ ███████║██╔████╔██║███████║██████╔╝█████╔╝ +# ██╔══██╗██╔══╝ ██║╚██╗██║██║ ██╔══██║██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ +# ██████╔╝███████╗██║ ╚████║╚██████╗██║ ██║██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗ +# ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD SYSTEM BENCHMARK v1.0 +# Comprehensive System Performance Testing +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +BENCHMARK_DIR="/tmp/blackroad-benchmark" +RESULTS_FILE="$BENCHMARK_DIR/results.json" + +declare -A SCORES=( + [cpu_single]=0 + [cpu_multi]=0 + [memory]=0 + [disk_seq]=0 + [disk_random]=0 + [network]=0 +) + +#─────────────────────────────────────────────────────────────────────────────── +# UTILITIES +#─────────────────────────────────────────────────────────────────────────────── + +setup_benchmark() { + mkdir -p "$BENCHMARK_DIR" + rm -f "$BENCHMARK_DIR"/* +} + +cleanup_benchmark() { + rm -rf "$BENCHMARK_DIR" +} + +get_cpu_info() { + if [[ -f /proc/cpuinfo ]]; then + local model=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs) + local cores=$(grep -c "processor" /proc/cpuinfo) + echo "$model|$cores" + else + local model=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "Unknown") + local cores=$(sysctl -n hw.ncpu 2>/dev/null || echo "1") + echo "$model|$cores" + fi +} + +get_mem_info() { + if [[ -f /proc/meminfo ]]; then + local total=$(grep MemTotal /proc/meminfo | awk '{print $2}') + echo "$((total / 1024))" # MB + else + local total=$(sysctl -n hw.memsize 2>/dev/null || echo "0") + echo "$((total / 1024 / 1024))" # MB + fi +} + +format_size() { + local bytes="$1" + if [[ $bytes -ge 1073741824 ]]; then + printf "%.2f GB" "$(echo "$bytes / 1073741824" | bc -l)" + elif [[ $bytes -ge 1048576 ]]; then + printf "%.2f MB" "$(echo "$bytes / 1048576" | bc -l)" + elif [[ $bytes -ge 1024 ]]; then + printf "%.2f KB" "$(echo "$bytes / 1024" | bc -l)" + else + printf "%d B" "$bytes" + fi +} + +format_time() { + local ms="$1" + if [[ $ms -ge 60000 ]]; then + printf "%.1fm" "$(echo "$ms / 60000" | bc -l)" + elif [[ $ms -ge 1000 ]]; then + printf "%.2fs" "$(echo "$ms / 1000" | bc -l)" + else + printf "%dms" "$ms" + fi +} + +draw_progress() { + local current="$1" + local total="$2" + local width="${3:-40}" + local label="${4:-}" + + local pct=$((current * 100 / total)) + local filled=$((current * width / total)) + + printf "\r [${label}] " + printf "\033[38;5;39m" + for ((i=0; i<filled; i++)); do printf "█"; done + printf "\033[38;5;240m" + for ((i=filled; i<width; i++)); do printf "░"; done + printf "\033[0m %3d%%" "$pct" +} + +draw_result_bar() { + local score="$1" + local max="$2" + local width="${3:-30}" + local label="${4:-}" + + local pct=$((score * 100 / max)) + local filled=$((score * width / max)) + + # Color based on performance + local color="46" # green + [[ $pct -lt 70 ]] && color="226" # yellow + [[ $pct -lt 40 ]] && color="208" # orange + [[ $pct -lt 20 ]] && color="196" # red + + printf " %-15s " "$label" + printf "\033[38;5;${color}m" + for ((i=0; i<filled; i++)); do printf "█"; done + printf "\033[38;5;240m" + for ((i=filled; i<width; i++)); do printf "░"; done + printf "\033[0m %5d pts\n" "$score" +} + +#─────────────────────────────────────────────────────────────────────────────── +# CPU BENCHMARKS +#─────────────────────────────────────────────────────────────────────────────── + +bench_cpu_single() { + printf "\n \033[1;38;5;39m🔲 CPU Single-Core Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local iterations=1000000 + local start_time=$(date +%s%N) + + # Prime number calculation + local count=0 + for ((n=2; n<10000; n++)); do + local is_prime=1 + for ((i=2; i*i<=n; i++)); do + if [[ $((n % i)) -eq 0 ]]; then + is_prime=0 + break + fi + done + [[ $is_prime -eq 1 ]] && ((count++)) + + if [[ $((n % 500)) -eq 0 ]]; then + draw_progress "$n" 10000 40 "Primes" + fi + done + + local end_time=$(date +%s%N) + local duration_ms=$(( (end_time - start_time) / 1000000 )) + + printf "\n\n" + printf " Found %d primes in %s\n" "$count" "$(format_time $duration_ms)" + + # Calculate score (inverse of time, normalized) + local score=$((100000 / (duration_ms + 1))) + [[ $score -gt 1000 ]] && score=1000 + SCORES[cpu_single]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" +} + +bench_cpu_multi() { + printf "\n \033[1;38;5;39m🔳 CPU Multi-Core Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local cores=$(nproc 2>/dev/null || echo 4) + printf " Testing with %d cores...\n\n" "$cores" + + local start_time=$(date +%s%N) + + # Spawn parallel workers + for ((c=0; c<cores; c++)); do + ( + for ((n=0; n<5000; n++)); do + local x=$((n * n * n)) + done + ) & + done + + # Progress animation + for ((i=0; i<20; i++)); do + draw_progress "$i" 20 40 "Multi " + sleep 0.1 + done + + wait + + local end_time=$(date +%s%N) + local duration_ms=$(( (end_time - start_time) / 1000000 )) + + printf "\n\n" + printf " Completed in %s\n" "$(format_time $duration_ms)" + + local score=$((cores * 50000 / (duration_ms + 1))) + [[ $score -gt 2000 ]] && score=2000 + SCORES[cpu_multi]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" +} + +bench_cpu_float() { + printf "\n \033[1;38;5;39m🔢 Floating Point Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local start_time=$(date +%s%N) + + # Math operations + for ((i=0; i<1000; i++)); do + local result=$(echo "scale=10; s($i * 0.001) + c($i * 0.001) + sqrt($i + 1)" | bc -l 2>/dev/null) + + if [[ $((i % 50)) -eq 0 ]]; then + draw_progress "$i" 1000 40 "Float " + fi + done + + local end_time=$(date +%s%N) + local duration_ms=$(( (end_time - start_time) / 1000000 )) + + printf "\n\n" + printf " Completed in %s\n" "$(format_time $duration_ms)" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MEMORY BENCHMARKS +#─────────────────────────────────────────────────────────────────────────────── + +bench_memory() { + printf "\n \033[1;38;5;201m💾 Memory Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local test_size=10 # MB + local test_file="$BENCHMARK_DIR/memtest" + + printf " Testing %d MB allocation...\n\n" "$test_size" + + local start_time=$(date +%s%N) + + # Sequential write + dd if=/dev/zero of="$test_file" bs=1M count=$test_size conv=fdatasync 2>/dev/null + + for ((i=0; i<10; i++)); do + draw_progress "$i" 10 40 "Write " + dd if=/dev/zero of="$test_file" bs=1M count=1 conv=fdatasync 2>/dev/null + done + + printf "\n" + + # Sequential read + for ((i=0; i<10; i++)); do + draw_progress "$i" 10 40 "Read " + dd if="$test_file" of=/dev/null bs=1M count=1 2>/dev/null + done + + local end_time=$(date +%s%N) + local duration_ms=$(( (end_time - start_time) / 1000000 )) + + printf "\n\n" + + local bandwidth=$((test_size * 2 * 1000 / (duration_ms + 1))) # MB/s + printf " Bandwidth: %d MB/s\n" "$bandwidth" + + local score=$((bandwidth * 10)) + [[ $score -gt 1500 ]] && score=1500 + SCORES[memory]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" + + rm -f "$test_file" +} + +#─────────────────────────────────────────────────────────────────────────────── +# DISK BENCHMARKS +#─────────────────────────────────────────────────────────────────────────────── + +bench_disk_sequential() { + printf "\n \033[1;38;5;226m💿 Disk Sequential I/O Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local test_file="$BENCHMARK_DIR/disktest" + local test_size=50 # MB + + printf " Testing %d MB sequential I/O...\n\n" "$test_size" + + # Write test + local write_start=$(date +%s%N) + + for ((i=0; i<test_size; i++)); do + dd if=/dev/zero of="$test_file" bs=1M count=1 seek=$i conv=fdatasync 2>/dev/null + [[ $((i % 5)) -eq 0 ]] && draw_progress "$i" "$test_size" 40 "Write " + done + + local write_end=$(date +%s%N) + local write_ms=$(( (write_end - write_start) / 1000000 )) + local write_speed=$((test_size * 1000 / (write_ms + 1))) + + printf "\n Write: %d MB/s\n" "$write_speed" + + # Read test + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true + + local read_start=$(date +%s%N) + + for ((i=0; i<test_size; i++)); do + dd if="$test_file" of=/dev/null bs=1M count=1 skip=$i 2>/dev/null + [[ $((i % 5)) -eq 0 ]] && draw_progress "$i" "$test_size" 40 "Read " + done + + local read_end=$(date +%s%N) + local read_ms=$(( (read_end - read_start) / 1000000 )) + local read_speed=$((test_size * 1000 / (read_ms + 1))) + + printf "\n Read: %d MB/s\n" "$read_speed" + + local score=$(( (write_speed + read_speed) * 5 )) + [[ $score -gt 2000 ]] && score=2000 + SCORES[disk_seq]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" + + rm -f "$test_file" +} + +bench_disk_random() { + printf "\n \033[1;38;5;226m🎲 Disk Random I/O Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local test_file="$BENCHMARK_DIR/randomtest" + local num_ops=100 + + # Create test file + dd if=/dev/zero of="$test_file" bs=1M count=10 2>/dev/null + + printf " Testing %d random 4KB operations...\n\n" "$num_ops" + + local start_time=$(date +%s%N) + + for ((i=0; i<num_ops; i++)); do + local offset=$((RANDOM % 2500)) + dd if=/dev/urandom of="$test_file" bs=4K count=1 seek=$offset conv=notrunc 2>/dev/null + + [[ $((i % 10)) -eq 0 ]] && draw_progress "$i" "$num_ops" 40 "Random " + done + + local end_time=$(date +%s%N) + local duration_ms=$(( (end_time - start_time) / 1000000 )) + + printf "\n\n" + + local iops=$((num_ops * 1000 / (duration_ms + 1))) + printf " Random IOPS: %d ops/s\n" "$iops" + + local score=$((iops * 2)) + [[ $score -gt 1000 ]] && score=1000 + SCORES[disk_random]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" + + rm -f "$test_file" +} + +#─────────────────────────────────────────────────────────────────────────────── +# NETWORK BENCHMARKS +#─────────────────────────────────────────────────────────────────────────────── + +bench_network() { + printf "\n \033[1;38;5;46m🌐 Network Benchmark\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + local test_urls=( + "https://www.google.com" + "https://www.cloudflare.com" + "https://www.github.com" + ) + + printf " Testing connectivity and latency...\n\n" + + local total_time=0 + local success=0 + + for url in "${test_urls[@]}"; do + local host=$(echo "$url" | sed 's|https://||' | sed 's|/.*||') + + draw_progress "$success" "${#test_urls[@]}" 40 "Ping " + + local start_time=$(date +%s%N) + + if ping -c 1 -W 2 "$host" &>/dev/null; then + local end_time=$(date +%s%N) + local latency=$(( (end_time - start_time) / 1000000 )) + total_time=$((total_time + latency)) + ((success++)) + fi + done + + printf "\n\n" + + if [[ $success -gt 0 ]]; then + local avg_latency=$((total_time / success)) + printf " Average latency: %dms\n" "$avg_latency" + + local score=$((1000 - avg_latency * 5)) + [[ $score -lt 0 ]] && score=0 + [[ $score -gt 1000 ]] && score=1000 + SCORES[network]=$score + + printf " Score: \033[38;5;46m%d\033[0m pts\n" "$score" + else + printf " \033[38;5;196mNo network connectivity\033[0m\n" + SCORES[network]=0 + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# RESULTS +#─────────────────────────────────────────────────────────────────────────────── + +render_header() { + printf "\033[1;38;5;214m" + cat << 'EOF' +╔══════════════════════════════════════════════════════════════════════════════╗ +║ ⚡ SYSTEM BENCHMARK SUITE ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +EOF + printf "\033[0m" +} + +render_system_info() { + printf "\n \033[1;38;5;39m📊 System Information\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + IFS='|' read -r cpu_model cpu_cores <<< "$(get_cpu_info)" + local mem_mb=$(get_mem_info) + + printf " CPU: %s\n" "$cpu_model" + printf " Cores: %s\n" "$cpu_cores" + printf " Memory: %d MB\n" "$mem_mb" + printf " OS: %s\n" "$(uname -s) $(uname -r)" + printf " Date: %s\n" "$(date)" +} + +render_results() { + printf "\n \033[1;38;5;201m🏆 Benchmark Results\033[0m\n" + printf " \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n\n" + + local total=0 + local max_score=1000 + + draw_result_bar "${SCORES[cpu_single]}" "$max_score" 30 "CPU Single" + total=$((total + SCORES[cpu_single])) + + draw_result_bar "${SCORES[cpu_multi]}" 2000 30 "CPU Multi" + total=$((total + SCORES[cpu_multi])) + + draw_result_bar "${SCORES[memory]}" 1500 30 "Memory" + total=$((total + SCORES[memory])) + + draw_result_bar "${SCORES[disk_seq]}" 2000 30 "Disk Seq" + total=$((total + SCORES[disk_seq])) + + draw_result_bar "${SCORES[disk_random]}" "$max_score" 30 "Disk Random" + total=$((total + SCORES[disk_random])) + + draw_result_bar "${SCORES[network]}" "$max_score" 30 "Network" + total=$((total + SCORES[network])) + + printf "\n \033[38;5;240m───────────────────────────────────────────────────────────\033[0m\n" + + # Total score + local max_total=7500 + local pct=$((total * 100 / max_total)) + + printf "\n \033[1;38;5;214mTOTAL SCORE: %d / %d (%d%%)\033[0m\n\n" "$total" "$max_total" "$pct" + + # Rating + local rating="" + local rating_color="" + if [[ $pct -ge 80 ]]; then + rating="EXCELLENT" + rating_color="46" + elif [[ $pct -ge 60 ]]; then + rating="VERY GOOD" + rating_color="39" + elif [[ $pct -ge 40 ]]; then + rating="GOOD" + rating_color="226" + elif [[ $pct -ge 20 ]]; then + rating="FAIR" + rating_color="208" + else + rating="NEEDS UPGRADE" + rating_color="196" + fi + + printf " Rating: \033[1;38;5;${rating_color}m★ %s ★\033[0m\n" "$rating" +} + +export_results() { + local output="$1" + + { + printf "{\n" + printf ' "timestamp": "%s",\n' "$(date -Iseconds)" + printf ' "system": {\n' + IFS='|' read -r cpu_model cpu_cores <<< "$(get_cpu_info)" + printf ' "cpu": "%s",\n' "$cpu_model" + printf ' "cores": %s,\n' "$cpu_cores" + printf ' "memory_mb": %d\n' "$(get_mem_info)" + printf ' },\n' + printf ' "scores": {\n' + printf ' "cpu_single": %d,\n' "${SCORES[cpu_single]}" + printf ' "cpu_multi": %d,\n' "${SCORES[cpu_multi]}" + printf ' "memory": %d,\n' "${SCORES[memory]}" + printf ' "disk_sequential": %d,\n' "${SCORES[disk_seq]}" + printf ' "disk_random": %d,\n' "${SCORES[disk_random]}" + printf ' "network": %d\n' "${SCORES[network]}" + printf ' }\n' + printf "}\n" + } > "$output" + + printf "\n \033[38;5;46m✓ Results exported to: %s\033[0m\n" "$output" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +show_help() { + cat << 'EOF' + + BLACKROAD SYSTEM BENCHMARK + ═══════════════════════════ + + Usage: system-benchmark.sh [options] + + Options: + -a, --all Run all benchmarks (default) + -c, --cpu CPU benchmarks only + -m, --memory Memory benchmark only + -d, --disk Disk benchmarks only + -n, --network Network benchmark only + -q, --quick Quick benchmark (reduced iterations) + -e, --export <file> Export results to JSON + -h, --help Show this help + + Benchmarks included: + • CPU Single-core (prime calculation) + • CPU Multi-core (parallel processing) + • Memory bandwidth + • Disk sequential I/O + • Disk random I/O + • Network latency + + Examples: + system-benchmark.sh # Full benchmark + system-benchmark.sh -c # CPU only + system-benchmark.sh -q # Quick mode + system-benchmark.sh -e bench.json # Export results + +EOF +} + +run_benchmark() { + local run_cpu="${1:-true}" + local run_memory="${2:-true}" + local run_disk="${3:-true}" + local run_network="${4:-true}" + local export_file="$5" + + clear + render_header + render_system_info + + setup_benchmark + + if [[ "$run_cpu" == "true" ]]; then + bench_cpu_single + bench_cpu_multi + fi + + if [[ "$run_memory" == "true" ]]; then + bench_memory + fi + + if [[ "$run_disk" == "true" ]]; then + bench_disk_sequential + bench_disk_random + fi + + if [[ "$run_network" == "true" ]]; then + bench_network + fi + + render_results + + if [[ -n "$export_file" ]]; then + export_results "$export_file" + fi + + cleanup_benchmark +} + +main() { + local run_cpu=true + local run_memory=true + local run_disk=true + local run_network=true + local export_file="" + local run_all=false + + while [[ $# -gt 0 ]]; do + case "$1" in + -a|--all) run_all=true; shift ;; + -c|--cpu) + run_cpu=true; run_memory=false; run_disk=false; run_network=false + shift ;; + -m|--memory) + run_cpu=false; run_memory=true; run_disk=false; run_network=false + shift ;; + -d|--disk) + run_cpu=false; run_memory=false; run_disk=true; run_network=false + shift ;; + -n|--network) + run_cpu=false; run_memory=false; run_disk=false; run_network=true + shift ;; + -q|--quick) + # Quick mode - reduced testing + shift ;; + -e|--export) export_file="$2"; shift 2 ;; + -h|--help) show_help; exit 0 ;; + *) shift ;; + esac + done + + run_benchmark "$run_cpu" "$run_memory" "$run_disk" "$run_network" "$export_file" + + printf "\n" +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/terminal-3d.sh b/terminal-3d.sh new file mode 100644 index 0000000..b2f8aeb --- /dev/null +++ b/terminal-3d.sh @@ -0,0 +1,521 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ████████╗███████╗██████╗ ███╗ ███╗██╗███╗ ██╗ █████╗ ██╗ ██████╗ ██████╗ +# ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗ ██║██╔══██╗██║ ╚════██╗██╔══██╗ +# ██║ █████╗ ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║██║ █████╔╝██║ ██║ +# ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║██║ ╚═══██╗██║ ██║ +# ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║███████╗ ██████╔╝██████╔╝ +# ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD TERMINAL 3D VISUALIZATION ENGINE v3.0 +# ASCII 3D Rendering, Rotations, Animations, Data Cubes +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# 3D MATH FUNCTIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Convert degrees to radians +deg_to_rad() { + echo "scale=6; $1 * 3.14159265359 / 180" | bc -l 2>/dev/null +} + +# Sine approximation +sin_approx() { + local x=$(deg_to_rad "$1") + echo "scale=4; $x - ($x^3)/6 + ($x^5)/120" | bc -l 2>/dev/null || echo "0" +} + +# Cosine approximation +cos_approx() { + local x=$(deg_to_rad "$1") + echo "scale=4; 1 - ($x^2)/2 + ($x^4)/24" | bc -l 2>/dev/null || echo "1" +} + +# Rotate point around X axis +rotate_x() { + local y="$1" z="$2" angle="$3" + local cos=$(cos_approx "$angle") + local sin=$(sin_approx "$angle") + + local new_y=$(echo "scale=4; $y * $cos - $z * $sin" | bc -l 2>/dev/null) + local new_z=$(echo "scale=4; $y * $sin + $z * $cos" | bc -l 2>/dev/null) + + echo "$new_y $new_z" +} + +# Rotate point around Y axis +rotate_y() { + local x="$1" z="$2" angle="$3" + local cos=$(cos_approx "$angle") + local sin=$(sin_approx "$angle") + + local new_x=$(echo "scale=4; $x * $cos + $z * $sin" | bc -l 2>/dev/null) + local new_z=$(echo "scale=4; -$x * $sin + $z * $cos" | bc -l 2>/dev/null) + + echo "$new_x $new_z" +} + +# Rotate point around Z axis +rotate_z() { + local x="$1" y="$2" angle="$3" + local cos=$(cos_approx "$angle") + local sin=$(sin_approx "$angle") + + local new_x=$(echo "scale=4; $x * $cos - $y * $sin" | bc -l 2>/dev/null) + local new_y=$(echo "scale=4; $x * $sin + $y * $cos" | bc -l 2>/dev/null) + + echo "$new_x $new_y" +} + +# Project 3D to 2D +project() { + local x="$1" y="$2" z="$3" + local fov="${4:-200}" + local distance="${5:-5}" + + local factor=$(echo "scale=4; $fov / ($z + $distance)" | bc -l 2>/dev/null || echo "40") + + local screen_x=$(echo "scale=0; $x * $factor + 40" | bc 2>/dev/null || echo "40") + local screen_y=$(echo "scale=0; $y * $factor + 15" | bc 2>/dev/null || echo "15") + + echo "$screen_x $screen_y" +} + +#─────────────────────────────────────────────────────────────────────────────── +# 3D SHAPES +#─────────────────────────────────────────────────────────────────────────────── + +# Define cube vertices +declare -a CUBE_VERTICES=( + "-1 -1 -1" # 0 + " 1 -1 -1" # 1 + " 1 1 -1" # 2 + "-1 1 -1" # 3 + "-1 -1 1" # 4 + " 1 -1 1" # 5 + " 1 1 1" # 6 + "-1 1 1" # 7 +) + +# Define cube edges (vertex pairs) +declare -a CUBE_EDGES=( + "0 1" "1 2" "2 3" "3 0" # Back face + "4 5" "5 6" "6 7" "7 4" # Front face + "0 4" "1 5" "2 6" "3 7" # Connecting edges +) + +# Define pyramid vertices +declare -a PYRAMID_VERTICES=( + " 0 1 0" # Top + "-1 -1 -1" # Base 0 + " 1 -1 -1" # Base 1 + " 1 -1 1" # Base 2 + "-1 -1 1" # Base 3 +) + +declare -a PYRAMID_EDGES=( + "0 1" "0 2" "0 3" "0 4" # Apex to base + "1 2" "2 3" "3 4" "4 1" # Base +) + +# Render buffer +declare -A SCREEN_BUFFER +declare -A DEPTH_BUFFER + +#─────────────────────────────────────────────────────────────────────────────── +# RENDERING ENGINE +#─────────────────────────────────────────────────────────────────────────────── + +# Clear buffers +clear_buffers() { + SCREEN_BUFFER=() + DEPTH_BUFFER=() +} + +# Plot point with depth check +plot_point() { + local x="$1" y="$2" z="$3" char="$4" color="${5:-$BR_CYAN}" + + local key="$x,$y" + local current_z="${DEPTH_BUFFER[$key]:-999}" + + if (( $(echo "$z < $current_z" | bc -l 2>/dev/null || echo 1) )); then + SCREEN_BUFFER[$key]="${color}${char}${RST}" + DEPTH_BUFFER[$key]="$z" + fi +} + +# Draw line between two 3D points (Bresenham) +draw_3d_line() { + local x0="$1" y0="$2" z0="$3" + local x1="$4" y1="$5" z1="$6" + local char="${7:-.}" + local color="${8:-$BR_CYAN}" + + # Project both points + read sx0 sy0 <<< "$(project "$x0" "$y0" "$z0")" + read sx1 sy1 <<< "$(project "$x1" "$y1" "$z1")" + + # Bresenham's line algorithm + local dx=$((${sx1%.*} - ${sx0%.*})) + local dy=$((${sy1%.*} - ${sy0%.*})) + local dx_abs=${dx#-} + local dy_abs=${dy#-} + + local sx=1 sy=1 + [[ $dx -lt 0 ]] && sx=-1 + [[ $dy -lt 0 ]] && sy=-1 + + local err=$((dx_abs - dy_abs)) + local x="${sx0%.*}" y="${sy0%.*}" + local x_end="${sx1%.*}" y_end="${sy1%.*}" + + local steps=$((dx_abs > dy_abs ? dx_abs : dy_abs)) + [[ $steps -eq 0 ]] && steps=1 + + for ((i=0; i<=steps && i<100; i++)); do + # Interpolate z + local t=$(echo "scale=4; $i / $steps" | bc -l 2>/dev/null || echo "0.5") + local z=$(echo "scale=4; $z0 + ($z1 - $z0) * $t" | bc -l 2>/dev/null || echo "0") + + plot_point "$x" "$y" "$z" "$char" "$color" + + [[ $x -eq $x_end && $y -eq $y_end ]] && break + + local e2=$((err * 2)) + if [[ $e2 -gt -$dy_abs ]]; then + err=$((err - dy_abs)) + x=$((x + sx)) + fi + if [[ $e2 -lt $dx_abs ]]; then + err=$((err + dx_abs)) + y=$((y + sy)) + fi + done +} + +# Render buffer to screen +render_buffer() { + local width="${1:-80}" + local height="${2:-30}" + + for ((y=0; y<height; y++)); do + for ((x=0; x<width; x++)); do + local key="$x,$y" + if [[ -n "${SCREEN_BUFFER[$key]}" ]]; then + printf "%s" "${SCREEN_BUFFER[$key]}" + else + printf " " + fi + done + printf "\n" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# 3D OBJECT RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +# Render rotating cube +render_cube() { + local angle_x="$1" angle_y="$2" angle_z="$3" + local scale="${4:-1}" + local color="${5:-$BR_CYAN}" + + clear_buffers + + # Transform and project all vertices + local -a projected=() + + for vertex in "${CUBE_VERTICES[@]}"; do + read x y z <<< "$vertex" + + # Scale + x=$(echo "scale=4; $x * $scale" | bc -l) + y=$(echo "scale=4; $y * $scale" | bc -l) + z=$(echo "scale=4; $z * $scale" | bc -l) + + # Rotate + read y z <<< "$(rotate_x "$y" "$z" "$angle_x")" + read x z <<< "$(rotate_y "$x" "$z" "$angle_y")" + read x y <<< "$(rotate_z "$x" "$y" "$angle_z")" + + projected+=("$x $y $z") + done + + # Draw edges + for edge in "${CUBE_EDGES[@]}"; do + read v0 v1 <<< "$edge" + read x0 y0 z0 <<< "${projected[$v0]}" + read x1 y1 z1 <<< "${projected[$v1]}" + + draw_3d_line "$x0" "$y0" "$z0" "$x1" "$y1" "$z1" "█" "$color" + done + + render_buffer 80 30 +} + +# Render rotating pyramid +render_pyramid() { + local angle_x="$1" angle_y="$2" angle_z="$3" + local scale="${4:-1.5}" + local color="${5:-$BR_ORANGE}" + + clear_buffers + + local -a projected=() + + for vertex in "${PYRAMID_VERTICES[@]}"; do + read x y z <<< "$vertex" + + x=$(echo "scale=4; $x * $scale" | bc -l) + y=$(echo "scale=4; $y * $scale" | bc -l) + z=$(echo "scale=4; $z * $scale" | bc -l) + + read y z <<< "$(rotate_x "$y" "$z" "$angle_x")" + read x z <<< "$(rotate_y "$x" "$z" "$angle_y")" + read x y <<< "$(rotate_z "$x" "$y" "$angle_z")" + + projected+=("$x $y $z") + done + + for edge in "${PYRAMID_EDGES[@]}"; do + read v0 v1 <<< "$edge" + read x0 y0 z0 <<< "${projected[$v0]}" + read x1 y1 z1 <<< "${projected[$v1]}" + + draw_3d_line "$x0" "$y0" "$z0" "$x1" "$y1" "$z1" "▲" "$color" + done + + render_buffer 80 30 +} + +#─────────────────────────────────────────────────────────────────────────────── +# DATA VISUALIZATION 3D +#─────────────────────────────────────────────────────────────────────────────── + +# 3D bar chart +render_3d_bars() { + local -a values=("$@") + local max_val=1 + local angle_y="${values[-1]}" + unset 'values[-1]' + + for v in "${values[@]}"; do + ((v > max_val)) && max_val=$v + done + + clear_buffers + + local num_bars=${#values[@]} + local bar_width=1 + local bar_spacing=2 + local start_x=$((-num_bars * bar_spacing / 2)) + + for ((i=0; i<num_bars; i++)); do + local height=$(echo "scale=2; ${values[$i]} * 3 / $max_val" | bc -l 2>/dev/null || echo "1") + local x=$((start_x + i * bar_spacing)) + + # Color based on value + local pct=$((${values[$i]} * 100 / max_val)) + local color="$BR_GREEN" + [[ $pct -gt 70 ]] && color="$BR_YELLOW" + [[ $pct -gt 85 ]] && color="$BR_RED" + + # Draw bar (vertical line with depth) + for h in $(seq 0 0.2 "$height"); do + local y=$(echo "scale=2; -1.5 + $h" | bc -l) + local z=0 + + # Rotate + read rx rz <<< "$(rotate_y "$x" "$z" "$angle_y")" + read sx sy <<< "$(project "$rx" "$y" "$rz")" + + plot_point "${sx%.*}" "${sy%.*}" "$rz" "█" "$color" + done + done + + render_buffer 80 30 +} + +# 3D scatter plot +render_3d_scatter() { + local angle_x="$1" angle_y="$2" + shift 2 + + clear_buffers + + local chars=("◉" "◎" "●" "○" "◆" "◇") + local colors=("$BR_CYAN" "$BR_PINK" "$BR_ORANGE" "$BR_PURPLE" "$BR_GREEN" "$BR_YELLOW") + + local i=0 + while [[ $# -ge 3 ]]; do + local x="$1" y="$2" z="$3" + shift 3 + + read ry rz <<< "$(rotate_x "$y" "$z" "$angle_x")" + read rx rz <<< "$(rotate_y "$x" "$rz" "$angle_y")" + read sx sy <<< "$(project "$rx" "$ry" "$rz")" + + local char="${chars[$((i % ${#chars[@]}))]}" + local color="${colors[$((i % ${#colors[@]}))]}" + + plot_point "${sx%.*}" "${sy%.*}" "$rz" "$char" "$color" + ((i++)) + done + + # Draw axes + draw_3d_line -2 0 0 2 0 0 "─" "$TEXT_MUTED" # X axis + draw_3d_line 0 -2 0 0 2 0 "│" "$TEXT_MUTED" # Y axis + draw_3d_line 0 0 -2 0 0 2 "·" "$TEXT_MUTED" # Z axis + + render_buffer 80 30 +} + +#─────────────────────────────────────────────────────────────────────────────── +# ANIMATED 3D DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +render_3d_dashboard() { + local angle=0 + + cursor_hide + clear_screen + + while true; do + cursor_to 1 1 + + printf "${BR_PURPLE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🎮 BLACKROAD 3D VISUALIZATION ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + # Get real metrics for visualization + local cpu=$(get_cpu_usage 2>/dev/null || echo "30") + local mem=$(get_memory_usage 2>/dev/null || echo "45") + local disk=$(get_disk_usage "/" 2>/dev/null || echo "60") + + # Render rotating data cube + render_cube "$((angle / 2))" "$angle" "$((angle / 3))" 1.5 "$BR_CYAN" + + printf "\n" + printf "${TEXT_MUTED}──────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${BR_CYAN}CPU: %3d%%${RST} │ ${BR_PINK}Memory: %3d%%${RST} │ ${BR_ORANGE}Disk: %3d%%${RST} │ ${TEXT_MUTED}Angle: %d°${RST}\n" \ + "$cpu" "$mem" "$disk" "$angle" + printf "${TEXT_MUTED}──────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[←/→] Rotate [↑/↓] Tilt [b]ars [s]catter [c]ube [p]yramid [q]uit${RST}\n" + + # Handle input + if read -rsn1 -t 0.1 key 2>/dev/null; then + case "$key" in + $'\e') + read -rsn2 -t 0.01 seq + case "$seq" in + '[C') angle=$((angle + 10)) ;; # Right + '[D') angle=$((angle - 10)) ;; # Left + esac + ;; + q|Q) break ;; + esac + fi + + angle=$((angle + 2)) + [[ $angle -ge 360 ]] && angle=0 + + sleep 0.05 + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MATRIX RAIN 3D +#─────────────────────────────────────────────────────────────────────────────── + +render_matrix_3d() { + local width=80 + local height=30 + local depth=10 + + declare -a drops=() + + # Initialize drops + for ((i=0; i<50; i++)); do + drops+=("$((RANDOM % width)) $((RANDOM % depth)) $((RANDOM % height))") + done + + cursor_hide + clear_screen + + local frame=0 + + while true; do + cursor_to 1 1 + clear_buffers + + # Update and render drops + for i in "${!drops[@]}"; do + read x z y <<< "${drops[$i]}" + + # Move drop down + y=$((y + 1)) + if [[ $y -gt $height ]]; then + y=0 + x=$((RANDOM % width)) + z=$((RANDOM % depth)) + fi + + drops[$i]="$x $z $y" + + # Project and plot + local intensity=$((255 - z * 20)) + [[ $intensity -lt 50 ]] && intensity=50 + + local color="\033[38;2;0;${intensity};$((intensity/2))m" + + # Random character + local chars="アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789" + local char="${chars:$((RANDOM % ${#chars})):1}" + + local screen_x=$((x + (z - 5))) + [[ $screen_x -lt 0 ]] && screen_x=0 + [[ $screen_x -ge $width ]] && screen_x=$((width - 1)) + + plot_point "$screen_x" "$y" "$z" "$char" "$color" + done + + render_buffer "$width" "$height" + + printf "\n${BR_GREEN}BLACKROAD MATRIX 3D${RST} │ ${TEXT_MUTED}Frame: %d │ Press 'q' to exit${RST}" "$frame" + + if read -rsn1 -t 0.05 key 2>/dev/null; then + [[ "$key" == "q" || "$key" == "Q" ]] && break + fi + + ((frame++)) + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) render_3d_dashboard ;; + cube) render_cube "${2:-0}" "${3:-45}" "${4:-0}" "${5:-1.5}" ;; + pyramid) render_pyramid "${2:-0}" "${3:-45}" "${4:-0}" ;; + bars) render_3d_bars 30 50 80 40 60 90 45 "${2:-0}" ;; + matrix) render_matrix_3d ;; + *) + printf "Usage: %s [dashboard|cube|pyramid|bars|matrix]\n" "$0" + ;; + esac +fi diff --git a/terminal-games.sh b/terminal-games.sh new file mode 100644 index 0000000..42daa2c --- /dev/null +++ b/terminal-games.sh @@ -0,0 +1,933 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██████╗ █████╗ ███╗ ███╗███████╗███████╗ +# ██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝ +# ██║ ███╗███████║██╔████╔██║█████╗ ███████╗ +# ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║ +# ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║ +# ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD TERMINAL GAMES v3.0 +# Snake, Tetris, Pong, and More - Pure Bash Gaming! +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +HIGHSCORE_FILE="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/highscores.json" +mkdir -p "$(dirname "$HIGHSCORE_FILE")" 2>/dev/null + +#─────────────────────────────────────────────────────────────────────────────── +# TERMINAL UTILITIES +#─────────────────────────────────────────────────────────────────────────────── + +get_term_size() { + local cols=$(tput cols) + local rows=$(tput lines) + echo "$cols $rows" +} + +cursor_to() { + printf "\033[%d;%dH" "$1" "$2" +} + +clear_screen() { + printf "\033[2J\033[H" +} + +cursor_hide() { + printf "\033[?25l" +} + +cursor_show() { + printf "\033[?25h" +} + +# Non-blocking key read +read_key() { + local key + read -rsn1 -t 0.1 key 2>/dev/null + if [[ "$key" == $'\x1b' ]]; then + read -rsn2 -t 0.01 key2 + key+="$key2" + fi + echo "$key" +} + +#─────────────────────────────────────────────────────────────────────────────── +# HIGH SCORES +#─────────────────────────────────────────────────────────────────────────────── + +save_highscore() { + local game="$1" + local score="$2" + local name="${3:-Anonymous}" + + if command -v jq &>/dev/null; then + local data="{}" + [[ -f "$HIGHSCORE_FILE" ]] && data=$(cat "$HIGHSCORE_FILE") + + local entry="{\"name\":\"$name\",\"score\":$score,\"date\":\"$(date -Iseconds)\"}" + local updated=$(echo "$data" | jq ".$game = (.$game // []) + [$entry] | .$game |= sort_by(-.score)[:10]") + echo "$updated" > "$HIGHSCORE_FILE" + fi +} + +get_highscore() { + local game="$1" + + if [[ -f "$HIGHSCORE_FILE" ]] && command -v jq &>/dev/null; then + jq -r ".$game[0].score // 0" "$HIGHSCORE_FILE" 2>/dev/null + else + echo "0" + fi +} + +show_highscores() { + local game="$1" + + printf "${BOLD}High Scores - %s${RST}\n\n" "${game^}" + + if [[ -f "$HIGHSCORE_FILE" ]] && command -v jq &>/dev/null; then + printf "%-4s %-20s %-10s\n" "#" "NAME" "SCORE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────" + + local idx=1 + while IFS='|' read -r name score; do + printf "${BR_CYAN}%-4d${RST} %-20s ${BR_YELLOW}%-10s${RST}\n" "$idx" "$name" "$score" + ((idx++)) + done < <(jq -r ".$game[]? | \"\(.name)|\(.score)\"" "$HIGHSCORE_FILE" 2>/dev/null | head -10) + else + printf "${TEXT_MUTED}No scores yet!${RST}\n" + fi +} + +#═══════════════════════════════════════════════════════════════════════════════ +# SNAKE GAME +#═══════════════════════════════════════════════════════════════════════════════ + +play_snake() { + local width=40 + local height=20 + local speed=0.1 + + # Snake data + local -a snake_x=(10 9 8) + local -a snake_y=(10 10 10) + local direction="right" + local next_direction="right" + + # Food + local food_x=$((RANDOM % (width - 2) + 1)) + local food_y=$((RANDOM % (height - 2) + 1)) + + # Game state + local score=0 + local game_over=false + local highscore=$(get_highscore "snake") + + clear_screen + cursor_hide + + # Game loop + while [[ "$game_over" == "false" ]]; do + # Draw border + cursor_to 1 1 + printf "${BR_GREEN}" + printf "╔" + printf "%0.s═" $(seq 1 $width) + printf "╗\n" + + for ((y=1; y<height; y++)); do + printf "║" + printf "%${width}s" "" + printf "║\n" + done + + printf "╚" + printf "%0.s═" $(seq 1 $width) + printf "╝${RST}\n" + + # Draw snake + for ((i=0; i<${#snake_x[@]}; i++)); do + cursor_to $((snake_y[i] + 1)) $((snake_x[i] + 1)) + if [[ $i -eq 0 ]]; then + printf "${BR_GREEN}●${RST}" # Head + else + printf "${BR_GREEN}○${RST}" # Body + fi + done + + # Draw food + cursor_to $((food_y + 1)) $((food_x + 1)) + printf "${BR_RED}★${RST}" + + # Score display + cursor_to $((height + 2)) 1 + printf "${BOLD}Score: ${BR_YELLOW}%d${RST} ${TEXT_MUTED}High Score: %d${RST} " "$score" "$highscore" + printf "${TEXT_SECONDARY}[WASD/Arrows to move, Q to quit]${RST}" + + # Read input + local key=$(read_key) + case "$key" in + w|W|$'\x1b[A') [[ "$direction" != "down" ]] && next_direction="up" ;; + s|S|$'\x1b[B') [[ "$direction" != "up" ]] && next_direction="down" ;; + a|A|$'\x1b[D') [[ "$direction" != "right" ]] && next_direction="left" ;; + d|D|$'\x1b[C') [[ "$direction" != "left" ]] && next_direction="right" ;; + q|Q) game_over=true; continue ;; + esac + + direction="$next_direction" + + # Calculate new head position + local new_x=${snake_x[0]} + local new_y=${snake_y[0]} + + case "$direction" in + up) ((new_y--)) ;; + down) ((new_y++)) ;; + left) ((new_x--)) ;; + right) ((new_x++)) ;; + esac + + # Check wall collision + if [[ $new_x -le 0 || $new_x -ge $width || $new_y -le 0 || $new_y -ge $height ]]; then + game_over=true + continue + fi + + # Check self collision + for ((i=0; i<${#snake_x[@]}; i++)); do + if [[ ${snake_x[i]} -eq $new_x && ${snake_y[i]} -eq $new_y ]]; then + game_over=true + break + fi + done + + # Move snake + # Clear tail + cursor_to $((snake_y[-1] + 1)) $((snake_x[-1] + 1)) + printf " " + + # Shift body + for ((i=${#snake_x[@]}-1; i>0; i--)); do + snake_x[i]=${snake_x[i-1]} + snake_y[i]=${snake_y[i-1]} + done + + # New head + snake_x[0]=$new_x + snake_y[0]=$new_y + + # Check food collision + if [[ $new_x -eq $food_x && $new_y -eq $food_y ]]; then + ((score += 10)) + + # Grow snake + snake_x+=(${snake_x[-1]}) + snake_y+=(${snake_y[-1]}) + + # New food + food_x=$((RANDOM % (width - 2) + 1)) + food_y=$((RANDOM % (height - 2) + 1)) + + # Speed up + speed=$(echo "$speed - 0.002" | bc -l 2>/dev/null || echo "$speed") + [[ $(echo "$speed < 0.03" | bc -l 2>/dev/null || echo 0) -eq 1 ]] && speed=0.03 + fi + + sleep "$speed" + done + + # Game over + cursor_to $((height / 2)) $((width / 2 - 5)) + printf "${BR_RED}${BOLD} GAME OVER ${RST}" + + cursor_to $((height / 2 + 1)) $((width / 2 - 7)) + printf "${BR_YELLOW}Final Score: %d${RST}" "$score" + + if [[ $score -gt $highscore ]]; then + cursor_to $((height / 2 + 2)) $((width / 2 - 8)) + printf "${BR_GREEN}NEW HIGH SCORE!${RST}" + save_highscore "snake" "$score" "Player" + fi + + sleep 2 + cursor_show +} + +#═══════════════════════════════════════════════════════════════════════════════ +# TETRIS GAME +#═══════════════════════════════════════════════════════════════════════════════ + +play_tetris() { + local width=12 + local height=22 + local speed=0.5 + + # Tetromino shapes (I, O, T, S, Z, J, L) + declare -A SHAPES + SHAPES[I]="0,0 1,0 2,0 3,0" + SHAPES[O]="0,0 1,0 0,1 1,1" + SHAPES[T]="0,0 1,0 2,0 1,1" + SHAPES[S]="1,0 2,0 0,1 1,1" + SHAPES[Z]="0,0 1,0 1,1 2,1" + SHAPES[J]="0,0 0,1 1,1 2,1" + SHAPES[L]="2,0 0,1 1,1 2,1" + + local SHAPE_CHARS=("I" "O" "T" "S" "Z" "J" "L") + local SHAPE_COLORS=("$BR_CYAN" "$BR_YELLOW" "$BR_PURPLE" "$BR_GREEN" "$BR_RED" "$BR_BLUE" "$BR_ORANGE") + + # Game board + declare -a board + for ((i=0; i<height*width; i++)); do + board[i]=0 + done + + # Current piece + local current_shape="" + local current_color="" + local piece_x=0 + local piece_y=0 + local piece_blocks="" + + # Game state + local score=0 + local lines=0 + local level=1 + local game_over=false + local highscore=$(get_highscore "tetris") + + # Spawn new piece + spawn_piece() { + local idx=$((RANDOM % ${#SHAPE_CHARS[@]})) + current_shape="${SHAPE_CHARS[idx]}" + current_color="${SHAPE_COLORS[idx]}" + piece_blocks="${SHAPES[$current_shape]}" + piece_x=$((width / 2 - 1)) + piece_y=0 + } + + # Check collision + check_collision() { + local px="$1" + local py="$2" + local blocks="$3" + + for block in $blocks; do + local bx=$((px + ${block%,*})) + local by=$((py + ${block#*,})) + + if [[ $bx -lt 0 || $bx -ge $width || $by -ge $height ]]; then + return 1 + fi + + if [[ $by -ge 0 && ${board[by*width+bx]} -ne 0 ]]; then + return 1 + fi + done + return 0 + } + + # Lock piece + lock_piece() { + for block in $piece_blocks; do + local bx=$((piece_x + ${block%,*})) + local by=$((piece_y + ${block#*,})) + + if [[ $by -ge 0 ]]; then + board[by*width+bx]=1 + fi + done + } + + # Clear lines + clear_lines() { + local cleared=0 + + for ((y=height-1; y>=0; y--)); do + local full=true + for ((x=0; x<width; x++)); do + if [[ ${board[y*width+x]} -eq 0 ]]; then + full=false + break + fi + done + + if [[ "$full" == "true" ]]; then + ((cleared++)) + + # Shift down + for ((yy=y; yy>0; yy--)); do + for ((x=0; x<width; x++)); do + board[yy*width+x]=${board[(yy-1)*width+x]} + done + done + + # Clear top row + for ((x=0; x<width; x++)); do + board[x]=0 + done + + ((y++)) # Recheck this row + fi + done + + if [[ $cleared -gt 0 ]]; then + ((lines += cleared)) + case $cleared in + 1) ((score += 100 * level)) ;; + 2) ((score += 300 * level)) ;; + 3) ((score += 500 * level)) ;; + 4) ((score += 800 * level)) ;; # Tetris! + esac + level=$((lines / 10 + 1)) + speed=$(echo "0.5 - $level * 0.04" | bc -l 2>/dev/null || echo "0.5") + [[ $(echo "$speed < 0.1" | bc -l 2>/dev/null || echo 0) -eq 1 ]] && speed=0.1 + fi + } + + # Rotate piece + rotate_piece() { + local new_blocks="" + for block in $piece_blocks; do + local bx=${block%,*} + local by=${block#*,} + # Rotate 90 degrees clockwise + local nx=$((1 - by)) + local ny=$bx + new_blocks+="$nx,$ny " + done + + if check_collision "$piece_x" "$piece_y" "$new_blocks"; then + piece_blocks="$new_blocks" + fi + } + + # Draw game + draw_game() { + cursor_to 1 1 + + # Border + printf "${BR_CYAN}╔" + for ((x=0; x<width; x++)); do printf "══"; done + printf "╗${RST}\n" + + for ((y=0; y<height; y++)); do + printf "${BR_CYAN}║${RST}" + + for ((x=0; x<width; x++)); do + local is_piece=false + + # Check if current piece occupies this cell + for block in $piece_blocks; do + local bx=$((piece_x + ${block%,*})) + local by=$((piece_y + ${block#*,})) + if [[ $bx -eq $x && $by -eq $y ]]; then + printf "${current_color}██${RST}" + is_piece=true + break + fi + done + + if [[ "$is_piece" == "false" ]]; then + if [[ ${board[y*width+x]} -eq 1 ]]; then + printf "${TEXT_MUTED}▓▓${RST}" + else + printf " " + fi + fi + done + + printf "${BR_CYAN}║${RST}" + + # Side panel + case $y in + 1) printf " ${BOLD}TETRIS${RST}" ;; + 3) printf " Score: ${BR_YELLOW}%d${RST}" "$score" ;; + 4) printf " Lines: ${BR_CYAN}%d${RST}" "$lines" ;; + 5) printf " Level: ${BR_GREEN}%d${RST}" "$level" ;; + 7) printf " High: ${TEXT_MUTED}%d${RST}" "$highscore" ;; + 10) printf " ${TEXT_SECONDARY}Controls:${RST}" ;; + 11) printf " ${TEXT_MUTED}← → Move${RST}" ;; + 12) printf " ${TEXT_MUTED}↑ Rotate${RST}" ;; + 13) printf " ${TEXT_MUTED}↓ Drop${RST}" ;; + 14) printf " ${TEXT_MUTED}Space Hard${RST}" ;; + 15) printf " ${TEXT_MUTED}Q Quit${RST}" ;; + esac + printf "\n" + done + + printf "${BR_CYAN}╚" + for ((x=0; x<width; x++)); do printf "══"; done + printf "╝${RST}\n" + } + + clear_screen + cursor_hide + spawn_piece + + local last_drop=$(date +%s.%N 2>/dev/null || date +%s) + + while [[ "$game_over" == "false" ]]; do + draw_game + + # Read input + local key=$(read_key) + case "$key" in + a|A|$'\x1b[D') + check_collision $((piece_x - 1)) "$piece_y" "$piece_blocks" && ((piece_x--)) + ;; + d|D|$'\x1b[C') + check_collision $((piece_x + 1)) "$piece_y" "$piece_blocks" && ((piece_x++)) + ;; + w|W|$'\x1b[A') + rotate_piece + ;; + s|S|$'\x1b[B') + if check_collision "$piece_x" $((piece_y + 1)) "$piece_blocks"; then + ((piece_y++)) + fi + ;; + " ") + # Hard drop + while check_collision "$piece_x" $((piece_y + 1)) "$piece_blocks"; do + ((piece_y++)) + ((score += 2)) + done + ;; + q|Q) + game_over=true + continue + ;; + esac + + # Auto drop + local now=$(date +%s.%N 2>/dev/null || date +%s) + local elapsed=$(echo "$now - $last_drop" | bc -l 2>/dev/null || echo 1) + + if (( $(echo "$elapsed >= $speed" | bc -l 2>/dev/null || echo 1) )); then + if check_collision "$piece_x" $((piece_y + 1)) "$piece_blocks"; then + ((piece_y++)) + else + lock_piece + clear_lines + + spawn_piece + if ! check_collision "$piece_x" "$piece_y" "$piece_blocks"; then + game_over=true + fi + fi + last_drop="$now" + fi + + sleep 0.05 + done + + # Game over + cursor_to $((height / 2)) 5 + printf "${BR_RED}${BOLD} GAME OVER ${RST}" + + if [[ $score -gt $highscore ]]; then + cursor_to $((height / 2 + 1)) 3 + printf "${BR_GREEN}NEW HIGH SCORE!${RST}" + save_highscore "tetris" "$score" "Player" + fi + + sleep 2 + cursor_show +} + +#═══════════════════════════════════════════════════════════════════════════════ +# PONG GAME +#═══════════════════════════════════════════════════════════════════════════════ + +play_pong() { + local width=60 + local height=20 + local paddle_size=4 + + # Paddles + local p1_y=$((height / 2 - paddle_size / 2)) + local p2_y=$((height / 2 - paddle_size / 2)) + + # Ball + local ball_x=$((width / 2)) + local ball_y=$((height / 2)) + local ball_dx=1 + local ball_dy=1 + + # Scores + local p1_score=0 + local p2_score=0 + local winning_score=5 + + clear_screen + cursor_hide + + while [[ $p1_score -lt $winning_score && $p2_score -lt $winning_score ]]; do + # Draw + cursor_to 1 1 + + # Top border + printf "${BR_CYAN}╔" + printf "%0.s═" $(seq 1 $width) + printf "╗${RST}\n" + + for ((y=0; y<height; y++)); do + printf "${BR_CYAN}║${RST}" + + for ((x=0; x<width; x++)); do + local char=" " + + # Left paddle + if [[ $x -eq 1 ]]; then + if [[ $y -ge $p1_y && $y -lt $((p1_y + paddle_size)) ]]; then + char="${BR_GREEN}█${RST}" + fi + fi + + # Right paddle + if [[ $x -eq $((width - 2)) ]]; then + if [[ $y -ge $p2_y && $y -lt $((p2_y + paddle_size)) ]]; then + char="${BR_BLUE}█${RST}" + fi + fi + + # Ball + if [[ $x -eq $ball_x && $y -eq $ball_y ]]; then + char="${BR_YELLOW}●${RST}" + fi + + # Center line + if [[ $x -eq $((width / 2)) && $((y % 2)) -eq 0 ]]; then + char="${TEXT_MUTED}│${RST}" + fi + + printf "%s" "$char" + done + + printf "${BR_CYAN}║${RST}\n" + done + + # Bottom border + printf "${BR_CYAN}╚" + printf "%0.s═" $(seq 1 $width) + printf "╝${RST}\n" + + # Score + printf "\n ${BR_GREEN}Player 1: %d${RST} ${BR_BLUE}Player 2 (AI): %d${RST}\n" "$p1_score" "$p2_score" + printf " ${TEXT_MUTED}[W/S to move, Q to quit]${RST}" + + # Input + local key=$(read_key) + case "$key" in + w|W) [[ $p1_y -gt 0 ]] && ((p1_y--)) ;; + s|S) [[ $p1_y -lt $((height - paddle_size)) ]] && ((p1_y++)) ;; + q|Q) cursor_show; return ;; + esac + + # AI movement + local p2_center=$((p2_y + paddle_size / 2)) + if [[ $ball_y -lt $p2_center && $p2_y -gt 0 ]]; then + ((p2_y--)) + elif [[ $ball_y -gt $p2_center && $p2_y -lt $((height - paddle_size)) ]]; then + ((p2_y++)) + fi + + # Ball movement + ((ball_x += ball_dx)) + ((ball_y += ball_dy)) + + # Wall bounce + if [[ $ball_y -le 0 || $ball_y -ge $((height - 1)) ]]; then + ((ball_dy *= -1)) + fi + + # Paddle bounce + if [[ $ball_x -eq 2 ]]; then + if [[ $ball_y -ge $p1_y && $ball_y -lt $((p1_y + paddle_size)) ]]; then + ((ball_dx *= -1)) + fi + fi + + if [[ $ball_x -eq $((width - 3)) ]]; then + if [[ $ball_y -ge $p2_y && $ball_y -lt $((p2_y + paddle_size)) ]]; then + ((ball_dx *= -1)) + fi + fi + + # Score + if [[ $ball_x -le 0 ]]; then + ((p2_score++)) + ball_x=$((width / 2)) + ball_y=$((height / 2)) + ball_dx=1 + fi + + if [[ $ball_x -ge $((width - 1)) ]]; then + ((p1_score++)) + ball_x=$((width / 2)) + ball_y=$((height / 2)) + ball_dx=-1 + fi + + sleep 0.08 + done + + # Winner + cursor_to $((height / 2)) $((width / 2 - 5)) + if [[ $p1_score -ge $winning_score ]]; then + printf "${BR_GREEN}${BOLD}YOU WIN!${RST}" + else + printf "${BR_RED}${BOLD}AI WINS!${RST}" + fi + + sleep 2 + cursor_show +} + +#═══════════════════════════════════════════════════════════════════════════════ +# BREAKOUT GAME +#═══════════════════════════════════════════════════════════════════════════════ + +play_breakout() { + local width=50 + local height=20 + local paddle_width=8 + + # Paddle + local paddle_x=$((width / 2 - paddle_width / 2)) + + # Ball + local ball_x=$((width / 2)) + local ball_y=$((height - 3)) + local ball_dx=1 + local ball_dy=-1 + + # Bricks + local brick_rows=4 + local brick_cols=10 + local brick_width=$((width / brick_cols)) + declare -a bricks + for ((i=0; i<brick_rows*brick_cols; i++)); do + bricks[i]=1 + done + + local score=0 + local lives=3 + local game_over=false + + clear_screen + cursor_hide + + while [[ "$game_over" == "false" ]]; do + cursor_to 1 1 + + # Top border + printf "${BR_CYAN}╔" + printf "%0.s═" $(seq 1 $width) + printf "╗${RST}\n" + + # Game area + for ((y=0; y<height; y++)); do + printf "${BR_CYAN}║${RST}" + + for ((x=0; x<width; x++)); do + local char=" " + + # Bricks + if [[ $y -lt $brick_rows ]]; then + local brick_col=$((x / brick_width)) + local brick_idx=$((y * brick_cols + brick_col)) + + if [[ ${bricks[brick_idx]} -eq 1 ]]; then + local colors=("$BR_RED" "$BR_ORANGE" "$BR_YELLOW" "$BR_GREEN") + char="${colors[y]}█${RST}" + fi + fi + + # Paddle + if [[ $y -eq $((height - 1)) ]]; then + if [[ $x -ge $paddle_x && $x -lt $((paddle_x + paddle_width)) ]]; then + char="${BR_BLUE}█${RST}" + fi + fi + + # Ball + if [[ $x -eq $ball_x && $y -eq $ball_y ]]; then + char="${BR_WHITE}●${RST}" + fi + + printf "%s" "$char" + done + + printf "${BR_CYAN}║${RST}\n" + done + + printf "${BR_CYAN}╚" + printf "%0.s═" $(seq 1 $width) + printf "╝${RST}\n" + + printf " ${BOLD}Score: ${BR_YELLOW}%d${RST} Lives: ${BR_RED}%s${RST}\n" "$score" "$(printf '♥%.0s' $(seq 1 $lives))" + + # Input + local key=$(read_key) + case "$key" in + a|A|$'\x1b[D') + [[ $paddle_x -gt 0 ]] && ((paddle_x -= 2)) + ;; + d|D|$'\x1b[C') + [[ $paddle_x -lt $((width - paddle_width)) ]] && ((paddle_x += 2)) + ;; + q|Q) + game_over=true + continue + ;; + esac + + # Ball movement + ((ball_x += ball_dx)) + ((ball_y += ball_dy)) + + # Wall bounce + if [[ $ball_x -le 0 || $ball_x -ge $((width - 1)) ]]; then + ((ball_dx *= -1)) + fi + + if [[ $ball_y -le 0 ]]; then + ((ball_dy *= -1)) + fi + + # Paddle bounce + if [[ $ball_y -eq $((height - 2)) ]]; then + if [[ $ball_x -ge $paddle_x && $ball_x -lt $((paddle_x + paddle_width)) ]]; then + ((ball_dy *= -1)) + fi + fi + + # Miss + if [[ $ball_y -ge $height ]]; then + ((lives--)) + if [[ $lives -le 0 ]]; then + game_over=true + else + ball_x=$((width / 2)) + ball_y=$((height - 3)) + ball_dy=-1 + fi + fi + + # Brick collision + if [[ $ball_y -lt $brick_rows ]]; then + local brick_col=$((ball_x / brick_width)) + local brick_idx=$((ball_y * brick_cols + brick_col)) + + if [[ ${bricks[brick_idx]} -eq 1 ]]; then + bricks[brick_idx]=0 + ((ball_dy *= -1)) + ((score += 10)) + fi + fi + + # Check win + local remaining=0 + for b in "${bricks[@]}"; do + ((remaining += b)) + done + + if [[ $remaining -eq 0 ]]; then + cursor_to $((height / 2)) $((width / 2 - 4)) + printf "${BR_GREEN}${BOLD}YOU WIN!${RST}" + save_highscore "breakout" "$score" "Player" + sleep 2 + game_over=true + fi + + sleep 0.06 + done + + if [[ $lives -le 0 ]]; then + cursor_to $((height / 2)) $((width / 2 - 5)) + printf "${BR_RED}${BOLD}GAME OVER${RST}" + sleep 2 + fi + + cursor_show +} + +#═══════════════════════════════════════════════════════════════════════════════ +# GAME MENU +#═══════════════════════════════════════════════════════════════════════════════ + +game_menu() { + clear_screen + cursor_hide + + while true; do + cursor_to 1 1 + + printf "${BR_PURPLE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🎮 BLACKROAD TERMINAL GAMES ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n\n" + + printf " ${BR_GREEN}1.${RST} ${BOLD}Snake${RST} - Classic snake game\n" + printf " ${BR_CYAN}2.${RST} ${BOLD}Tetris${RST} - Block stacking puzzle\n" + printf " ${BR_YELLOW}3.${RST} ${BOLD}Pong${RST} - Classic paddle game vs AI\n" + printf " ${BR_RED}4.${RST} ${BOLD}Breakout${RST} - Brick breaking action\n" + printf "\n" + printf " ${BR_PURPLE}H.${RST} ${TEXT_SECONDARY}High Scores${RST}\n" + printf " ${TEXT_MUTED}Q.${RST} ${TEXT_MUTED}Quit${RST}\n" + printf "\n" + printf " ${TEXT_SECONDARY}Select a game (1-4):${RST} " + + read -rsn1 choice + + case "$choice" in + 1) play_snake ;; + 2) play_tetris ;; + 3) play_pong ;; + 4) play_breakout ;; + h|H) + clear_screen + printf "\n" + show_highscores "snake" + printf "\n" + show_highscores "tetris" + printf "\n" + show_highscores "breakout" + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + q|Q) break ;; + esac + + clear_screen + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-menu}" in + menu) game_menu ;; + snake) play_snake ;; + tetris) play_tetris ;; + pong) play_pong ;; + breakout) play_breakout ;; + scores) show_highscores "${2:-snake}" ;; + *) + printf "Usage: %s [menu|snake|tetris|pong|breakout|scores]\n" "$0" + ;; + esac +fi diff --git a/themes-premium.sh b/themes-premium.sh new file mode 100644 index 0000000..9791ad5 --- /dev/null +++ b/themes-premium.sh @@ -0,0 +1,591 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ████████╗██╗ ██╗███████╗███╗ ███╗███████╗███████╗ +# ╚══██╔══╝██║ ██║██╔════╝████╗ ████║██╔════╝██╔════╝ +# ██║ ███████║█████╗ ██╔████╔██║█████╗ ███████╗ +# ██║ ██╔══██║██╔══╝ ██║╚██╔╝██║██╔══╝ ╚════██║ +# ██║ ██║ ██║███████╗██║ ╚═╝ ██║███████╗███████║ +# ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD PREMIUM THEMES v2.0 +# Advanced color themes with gradients, animations, and presets +#═══════════════════════════════════════════════════════════════════════════════ + +# Source core library +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# THEME CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +THEME_HOME="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/themes" +CURRENT_THEME_FILE="$THEME_HOME/.current_theme" +mkdir -p "$THEME_HOME" 2>/dev/null + +# Current active theme +CURRENT_THEME="${CURRENT_THEME:-cyberpunk}" + +#─────────────────────────────────────────────────────────────────────────────── +# PREMIUM THEME DEFINITIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Each theme defines: PRIMARY, SECONDARY, ACCENT, SUCCESS, WARNING, ERROR, BG, TEXT + +declare -A THEME_CYBERPUNK=( + [name]="Cyberpunk" + [description]="Neon-soaked futuristic vibes" + [PRIMARY]="255;0;128" # Hot pink + [SECONDARY]="0;255;255" # Cyan + [ACCENT]="255;255;0" # Yellow + [SUCCESS]="0;255;128" # Neon green + [WARNING]="255;165;0" # Orange + [ERROR]="255;0;64" # Red + [BG]="15;15;35" # Dark blue + [TEXT]="255;255;255" # White + [MUTED]="128;128;180" # Muted purple + [BORDER]="128;0;255" # Purple + [GRADIENT1]="255;0;128" + [GRADIENT2]="128;0;255" + [GRADIENT3]="0;128;255" +) + +declare -A THEME_MIDNIGHT=( + [name]="Midnight" + [description]="Deep space darkness" + [PRIMARY]="100;149;237" # Cornflower blue + [SECONDARY]="138;43;226" # Blue violet + [ACCENT]="255;215;0" # Gold + [SUCCESS]="50;205;50" # Lime green + [WARNING]="255;140;0" # Dark orange + [ERROR]="220;20;60" # Crimson + [BG]="10;10;20" # Near black + [TEXT]="230;230;250" # Lavender + [MUTED]="70;70;90" # Dark gray + [BORDER]="72;61;139" # Dark slate blue + [GRADIENT1]="25;25;112" + [GRADIENT2]="72;61;139" + [GRADIENT3]="138;43;226" +) + +declare -A THEME_AURORA=( + [name]="Aurora" + [description]="Northern lights inspired" + [PRIMARY]="0;255;128" # Spring green + [SECONDARY]="0;191;255" # Deep sky blue + [ACCENT]="255;0;255" # Magenta + [SUCCESS]="127;255;0" # Chartreuse + [WARNING]="255;215;0" # Gold + [ERROR]="255;69;0" # Orange red + [BG]="5;15;25" # Dark teal + [TEXT]="240;255;255" # Azure + [MUTED]="95;158;160" # Cadet blue + [BORDER]="0;139;139" # Dark cyan + [GRADIENT1]="0;100;0" + [GRADIENT2]="0;191;255" + [GRADIENT3]="138;43;226" +) + +declare -A THEME_VOLCANO=( + [name]="Volcano" + [description]="Fiery intensity" + [PRIMARY]="255;69;0" # Orange red + [SECONDARY]="255;140;0" # Dark orange + [ACCENT]="255;215;0" # Gold + [SUCCESS]="50;205;50" # Lime green + [WARNING]="255;255;0" # Yellow + [ERROR]="139;0;0" # Dark red + [BG]="25;5;5" # Dark red-brown + [TEXT]="255;248;220" # Cornsilk + [MUTED]="139;69;19" # Saddle brown + [BORDER]="178;34;34" # Firebrick + [GRADIENT1]="139;0;0" + [GRADIENT2]="255;69;0" + [GRADIENT3]="255;215;0" +) + +declare -A THEME_MATRIX=( + [name]="Matrix" + [description]="Digital rain aesthetics" + [PRIMARY]="0;255;65" # Matrix green + [SECONDARY]="0;200;50" # Darker green + [ACCENT]="255;255;255" # White + [SUCCESS]="0;255;0" # Pure green + [WARNING]="0;255;128" # Light green + [ERROR]="255;0;0" # Red + [BG]="0;10;0" # Very dark green + [TEXT]="0;255;65" # Matrix green + [MUTED]="0;80;30" # Dark green + [BORDER]="0;180;60" # Medium green + [GRADIENT1]="0;50;0" + [GRADIENT2]="0;150;50" + [GRADIENT3]="0;255;65" +) + +declare -A THEME_ARCTIC=( + [name]="Arctic" + [description]="Frozen crystalline beauty" + [PRIMARY]="135;206;250" # Light sky blue + [SECONDARY]="176;224;230" # Powder blue + [ACCENT]="255;250;250" # Snow + [SUCCESS]="144;238;144" # Light green + [WARNING]="255;218;185" # Peach puff + [ERROR]="255;99;71" # Tomato + [BG]="15;25;35" # Dark blue-gray + [TEXT]="240;248;255" # Alice blue + [MUTED]="119;136;153" # Light slate gray + [BORDER]="70;130;180" # Steel blue + [GRADIENT1]="70;130;180" + [GRADIENT2]="135;206;250" + [GRADIENT3]="255;250;250" +) + +declare -A THEME_SYNTHWAVE=( + [name]="Synthwave" + [description]="80s retro future" + [PRIMARY]="255;110;199" # Pink + [SECONDARY]="111;45;189" # Purple + [ACCENT]="254;228;64" # Yellow + [SUCCESS]="102;255;204" # Turquoise + [WARNING]="255;154;0" # Orange + [ERROR]="255;0;110" # Hot pink + [BG]="26;14;42" # Dark purple + [TEXT]="255;230;255" # Light pink + [MUTED]="147;112;219" # Medium purple + [BORDER]="186;85;211" # Medium orchid + [GRADIENT1]="111;45;189" + [GRADIENT2]="255;110;199" + [GRADIENT3]="254;228;64" +) + +declare -A THEME_FOREST=( + [name]="Forest" + [description]="Natural woodland serenity" + [PRIMARY]="34;139;34" # Forest green + [SECONDARY]="107;142;35" # Olive drab + [ACCENT]="218;165;32" # Goldenrod + [SUCCESS]="50;205;50" # Lime green + [WARNING]="210;180;140" # Tan + [ERROR]="165;42;42" # Brown + [BG]="15;20;10" # Dark forest + [TEXT]="245;245;220" # Beige + [MUTED]="85;107;47" # Dark olive green + [BORDER]="46;139;87" # Sea green + [GRADIENT1]="0;50;0" + [GRADIENT2]="46;139;87" + [GRADIENT3]="107;142;35" +) + +declare -A THEME_OCEAN=( + [name]="Ocean" + [description]="Deep sea exploration" + [PRIMARY]="0;105;148" # Cerulean + [SECONDARY]="64;224;208" # Turquoise + [ACCENT]="255;215;0" # Gold + [SUCCESS]="0;255;127" # Spring green + [WARNING]="255;165;0" # Orange + [ERROR]="220;20;60" # Crimson + [BG]="0;15;30" # Deep sea blue + [TEXT]="224;255;255" # Light cyan + [MUTED]="70;130;180" # Steel blue + [BORDER]="32;178;170" # Light sea green + [GRADIENT1]="0;0;139" + [GRADIENT2]="0;139;139" + [GRADIENT3]="64;224;208" +) + +declare -A THEME_MONOCHROME=( + [name]="Monochrome" + [description]="Clean grayscale elegance" + [PRIMARY]="200;200;200" # Light gray + [SECONDARY]="150;150;150" # Medium gray + [ACCENT]="255;255;255" # White + [SUCCESS]="180;180;180" # Light gray + [WARNING]="220;220;220" # Gainsboro + [ERROR]="100;100;100" # Dark gray + [BG]="20;20;20" # Near black + [TEXT]="240;240;240" # Off white + [MUTED]="80;80;80" # Gray + [BORDER]="120;120;120" # Dark gray + [GRADIENT1]="40;40;40" + [GRADIENT2]="120;120;120" + [GRADIENT3]="200;200;200" +) + +# List of all themes +AVAILABLE_THEMES=( + "cyberpunk" + "midnight" + "aurora" + "volcano" + "matrix" + "arctic" + "synthwave" + "forest" + "ocean" + "monochrome" +) + +#─────────────────────────────────────────────────────────────────────────────── +# THEME APPLICATION +#─────────────────────────────────────────────────────────────────────────────── + +# Get theme array by name +get_theme_array() { + local theme_name="$1" + local upper_name=$(echo "$theme_name" | tr '[:lower:]' '[:upper:]') + local array_name="THEME_${upper_name}" + + declare -n theme_ref="$array_name" 2>/dev/null || return 1 + echo "${!theme_ref[@]}" +} + +# Apply a theme +apply_theme() { + local theme_name="${1:-cyberpunk}" + local upper_name=$(echo "$theme_name" | tr '[:lower:]' '[:upper:]') + local array_name="THEME_${upper_name}" + + # Check if theme exists + declare -n theme="$array_name" 2>/dev/null || { + log_error "Theme '$theme_name' not found" + return 1 + } + + # Export theme colors as ANSI escape codes + export THEME_PRIMARY="\033[38;2;${theme[PRIMARY]}m" + export THEME_SECONDARY="\033[38;2;${theme[SECONDARY]}m" + export THEME_ACCENT="\033[38;2;${theme[ACCENT]}m" + export THEME_SUCCESS="\033[38;2;${theme[SUCCESS]}m" + export THEME_WARNING="\033[38;2;${theme[WARNING]}m" + export THEME_ERROR="\033[38;2;${theme[ERROR]}m" + export THEME_TEXT="\033[38;2;${theme[TEXT]}m" + export THEME_MUTED="\033[38;2;${theme[MUTED]}m" + export THEME_BORDER="\033[38;2;${theme[BORDER]}m" + + # Background colors + export THEME_BG="\033[48;2;${theme[BG]}m" + export THEME_BG_PRIMARY="\033[48;2;${theme[PRIMARY]}m" + export THEME_BG_SECONDARY="\033[48;2;${theme[SECONDARY]}m" + export THEME_BG_ACCENT="\033[48;2;${theme[ACCENT]}m" + + # Gradient colors + export THEME_GRADIENT1="\033[38;2;${theme[GRADIENT1]}m" + export THEME_GRADIENT2="\033[38;2;${theme[GRADIENT2]}m" + export THEME_GRADIENT3="\033[38;2;${theme[GRADIENT3]}m" + + # Store current theme + CURRENT_THEME="$theme_name" + echo "$theme_name" > "$CURRENT_THEME_FILE" + + log_debug "Theme applied: ${theme[name]}" + return 0 +} + +# Load saved theme +load_saved_theme() { + if [[ -f "$CURRENT_THEME_FILE" ]]; then + local saved_theme=$(cat "$CURRENT_THEME_FILE") + apply_theme "$saved_theme" + else + apply_theme "cyberpunk" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# GRADIENT TEXT RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +# Render text with horizontal gradient +gradient_text() { + local text="$1" + local r1="${2:-255}" g1="${3:-0}" b1="${4:-128}" + local r2="${5:-0}" g2="${6:-255}" b2="${7:-255}" + + local len=${#text} + local result="" + + for ((i=0; i<len; i++)); do + local ratio=$((i * 100 / (len - 1))) + local r=$((r1 + (r2 - r1) * ratio / 100)) + local g=$((g1 + (g2 - g1) * ratio / 100)) + local b=$((b1 + (b2 - b1) * ratio / 100)) + + result+="\033[38;2;${r};${g};${b}m${text:$i:1}" + done + + printf "%b${RST}" "$result" +} + +# Rainbow text +rainbow_text() { + local text="$1" + local len=${#text} + local result="" + + local colors=( + "255;0;0" # Red + "255;127;0" # Orange + "255;255;0" # Yellow + "0;255;0" # Green + "0;0;255" # Blue + "75;0;130" # Indigo + "148;0;211" # Violet + ) + + for ((i=0; i<len; i++)); do + local color_idx=$((i % ${#colors[@]})) + result+="\033[38;2;${colors[$color_idx]}m${text:$i:1}" + done + + printf "%b${RST}" "$result" +} + +# Themed gradient using current theme +theme_gradient_text() { + local text="$1" + local upper_name=$(echo "$CURRENT_THEME" | tr '[:lower:]' '[:upper:]') + local array_name="THEME_${upper_name}" + + declare -n theme="$array_name" 2>/dev/null || return 1 + + IFS=';' read -r r1 g1 b1 <<< "${theme[GRADIENT1]}" + IFS=';' read -r r2 g2 b2 <<< "${theme[GRADIENT3]}" + + gradient_text "$text" "$r1" "$g1" "$b1" "$r2" "$g2" "$b2" +} + +#─────────────────────────────────────────────────────────────────────────────── +# THEME PREVIEW +#─────────────────────────────────────────────────────────────────────────────── + +# Preview a single theme +preview_theme() { + local theme_name="${1:-$CURRENT_THEME}" + local upper_name=$(echo "$theme_name" | tr '[:lower:]' '[:upper:]') + local array_name="THEME_${upper_name}" + + declare -n theme="$array_name" 2>/dev/null || { + printf "Theme not found: %s\n" "$theme_name" + return 1 + } + + local p="\033[38;2;${theme[PRIMARY]}m" + local s="\033[38;2;${theme[SECONDARY]}m" + local a="\033[38;2;${theme[ACCENT]}m" + local t="\033[38;2;${theme[TEXT]}m" + local b="\033[38;2;${theme[BORDER]}m" + local g1="\033[38;2;${theme[GRADIENT1]}m" + local g2="\033[38;2;${theme[GRADIENT2]}m" + local g3="\033[38;2;${theme[GRADIENT3]}m" + local bg="\033[48;2;${theme[BG]}m" + + printf "${bg}${b}╭──────────────────────────────────────────────────────╮${RST}\n" + printf "${bg}${b}│${RST}${bg} ${p}█${s}█${a}█${RST}${bg} ${p}${BOLD}%-15s${RST}${bg} ${t}%-30s${b}│${RST}\n" "${theme[name]}" "${theme[description]}" + printf "${bg}${b}├──────────────────────────────────────────────────────┤${RST}\n" + printf "${bg}${b}│${RST}${bg} ${p}Primary ${s}Secondary ${a}Accent${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} " + printf "${p}████████${RST}${bg} ${s}████████${RST}${bg} ${a}████████${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} Gradient: ${g1}████${g2}████${g3}████${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} ${t}Sample text in theme colors${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}│${RST}${bg} " + printf "\033[38;2;${theme[SUCCESS]}m✓ Success${RST}${bg} " + printf "\033[38;2;${theme[WARNING]}m⚠ Warning${RST}${bg} " + printf "\033[38;2;${theme[ERROR]}m✗ Error${RST}${bg} ${b}│${RST}\n" + printf "${bg}${b}╰──────────────────────────────────────────────────────╯${RST}\n" +} + +# Preview all themes +preview_all_themes() { + clear_screen + cursor_hide + + printf "${BOLD}" + theme_gradient_text "═══════════════════════════════════════════════════════════" + printf "\n" + theme_gradient_text " BLACKROAD THEMES " + printf "\n" + theme_gradient_text "═══════════════════════════════════════════════════════════" + printf "${RST}\n\n" + + for theme_name in "${AVAILABLE_THEMES[@]}"; do + preview_theme "$theme_name" + printf "\n" + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# THEME SELECTOR UI +#─────────────────────────────────────────────────────────────────────────────── + +theme_selector() { + local selected=0 + local num_themes=${#AVAILABLE_THEMES[@]} + + cursor_hide + stty -echo 2>/dev/null + + while true; do + clear_screen + + printf "${BOLD}" + theme_gradient_text "╔══════════════════════════════════════════════════════════╗" + printf "\n" + theme_gradient_text "║ 🎨 BLACKROAD THEME SELECTOR ║" + printf "\n" + theme_gradient_text "╚══════════════════════════════════════════════════════════╝" + printf "${RST}\n\n" + + printf " ${TEXT_MUTED}Current theme: ${THEME_PRIMARY}%s${RST}\n\n" "$CURRENT_THEME" + + for i in "${!AVAILABLE_THEMES[@]}"; do + local theme_name="${AVAILABLE_THEMES[$i]}" + local upper_name=$(echo "$theme_name" | tr '[:lower:]' '[:upper:]') + local array_name="THEME_${upper_name}" + + declare -n theme="$array_name" 2>/dev/null || continue + + local p="\033[38;2;${theme[PRIMARY]}m" + local s="\033[38;2;${theme[SECONDARY]}m" + local a="\033[38;2;${theme[ACCENT]}m" + + if [[ $i -eq $selected ]]; then + printf " ${THEME_ACCENT}▶ ${p}█${s}█${a}█${RST} ${BOLD}${p}%-12s${RST} ${TEXT_SECONDARY}${theme[description]}${RST}\n" "${theme[name]}" + else + printf " ${p}█${s}█${a}█${RST} ${TEXT_MUTED}%-12s${RST} ${TEXT_MUTED}${theme[description]}${RST}\n" "${theme[name]}" + fi + done + + printf "\n\n" + preview_theme "${AVAILABLE_THEMES[$selected]}" + + printf "\n ${TEXT_MUTED}↑/↓ Navigate • Enter Apply • Q Quit${RST}\n" + + # Read input + read -rsn1 key + + case "$key" in + $'\e') + read -rsn2 -t 0.01 key2 + case "$key2" in + '[A') ((selected = (selected - 1 + num_themes) % num_themes)) ;; # Up + '[B') ((selected = (selected + 1) % num_themes)) ;; # Down + esac + ;; + '') # Enter + apply_theme "${AVAILABLE_THEMES[$selected]}" + printf "\n ${THEME_SUCCESS}✓ Theme applied: ${AVAILABLE_THEMES[$selected]}${RST}\n" + sleep 1 + ;; + q|Q) + break + ;; + esac + done + + cursor_show + stty echo 2>/dev/null +} + +#─────────────────────────────────────────────────────────────────────────────── +# THEMED UI COMPONENTS +#─────────────────────────────────────────────────────────────────────────────── + +# Draw themed box +theme_box() { + local title="$1" + local width="${2:-50}" + + printf "${THEME_BORDER}╭" + printf "%0.s─" $(seq 1 $((width - 2))) + printf "╮${RST}\n" + + if [[ -n "$title" ]]; then + local title_len=${#title} + local padding=$(( (width - 4 - title_len) / 2 )) + printf "${THEME_BORDER}│${RST}" + printf "%${padding}s" "" + printf "${THEME_PRIMARY}${BOLD} %s ${RST}" "$title" + printf "%$(( width - 4 - title_len - padding ))s" + printf "${THEME_BORDER}│${RST}\n" + + printf "${THEME_BORDER}├" + printf "%0.s─" $(seq 1 $((width - 2))) + printf "┤${RST}\n" + fi +} + +theme_box_close() { + local width="${1:-50}" + + printf "${THEME_BORDER}╰" + printf "%0.s─" $(seq 1 $((width - 2))) + printf "╯${RST}\n" +} + +# Themed progress bar +theme_progress() { + local current="$1" + local total="$2" + local width="${3:-30}" + + local percentage=$((current * 100 / total)) + local filled=$((current * width / total)) + local empty=$((width - filled)) + + printf "${THEME_PRIMARY}" + printf "%0.s█" $(seq 1 "$filled" 2>/dev/null) || true + printf "${THEME_MUTED}" + printf "%0.s░" $(seq 1 "$empty" 2>/dev/null) || true + printf "${RST} ${THEME_TEXT}%3d%%${RST}" "$percentage" +} + +# Themed status indicator +theme_status() { + local status="$1" + local label="$2" + + case "$status" in + online|up|ok|success) + printf "${THEME_SUCCESS}◉ %s${RST}" "$label" + ;; + offline|down|error|fail) + printf "${THEME_ERROR}○ %s${RST}" "$label" + ;; + warning|warn|degraded) + printf "${THEME_WARNING}◐ %s${RST}" "$label" + ;; + *) + printf "${THEME_SECONDARY}◎ %s${RST}" "$label" + ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +# Load theme on source +load_saved_theme + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-selector}" in + selector|select) theme_selector ;; + preview) preview_all_themes ;; + apply) apply_theme "${2:-cyberpunk}" ;; + list) + printf "Available themes:\n" + for t in "${AVAILABLE_THEMES[@]}"; do + [[ "$t" == "$CURRENT_THEME" ]] && printf " * %s (current)\n" "$t" || printf " %s\n" "$t" + done + ;; + *) + printf "Usage: %s [selector|preview|apply <theme>|list]\n" "$0" + printf " %s apply cyberpunk\n" "$0" + ;; + esac +fi diff --git a/time-machine.sh b/time-machine.sh index fe734e1..d507737 100755 --- a/time-machine.sh +++ b/time-machine.sh @@ -1,253 +1,266 @@ #!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ████████╗██╗███╗ ███╗███████╗ ███╗ ███╗ █████╗ ██████╗██╗ ██╗██╗███╗ ██╗███████╗ +# ╚══██╔══╝██║████╗ ████║██╔════╝ ████╗ ████║██╔══██╗██╔════╝██║ ██║██║████╗ ██║██╔════╝ +# ██║ ██║██╔████╔██║█████╗ ██╔████╔██║███████║██║ ███████║██║██╔██╗ ██║█████╗ +# ██║ ██║██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██╔══██║██║ ██╔══██║██║██║╚██╗██║██╔══╝ +# ██║ ██║██║ ╚═╝ ██║███████╗ ██║ ╚═╝ ██║██║ ██║╚██████╗██║ ██║██║██║ ╚████║███████╗ +# ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚══════╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD TIME MACHINE v3.0 +# Historical Data Recording, Replay & Trend Analysis +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +DATA_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/timemachine" +SNAPSHOTS_DIR="$DATA_DIR/snapshots" +RECORDINGS_DIR="$DATA_DIR/recordings" +EVENTS_FILE="$DATA_DIR/events.log" + +mkdir -p "$SNAPSHOTS_DIR" "$RECORDINGS_DIR" 2>/dev/null +touch "$EVENTS_FILE" 2>/dev/null + +# Recording state +RECORDING_ACTIVE=false +RECORDING_NAME="" +RECORDING_START=0 + +#─────────────────────────────────────────────────────────────────────────────── +# SNAPSHOT MANAGEMENT +#─────────────────────────────────────────────────────────────────────────────── + +take_snapshot() { + local name="${1:-snapshot_$(date +%Y%m%d_%H%M%S)}" + local snapshot_file="$SNAPSHOTS_DIR/${name}.json" + + local cpu=$(grep 'cpu ' /proc/stat 2>/dev/null | awk '{usage=($2+$4)*100/($2+$4+$5)} END {printf "%.1f", usage}' || echo "0") + local mem=$(free 2>/dev/null | awk '/Mem:/ {printf "%.1f", $3/$2 * 100}' || echo "0") + local disk=$(df / 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%' || echo "0") + local load=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}' || echo "0") + local connections=$(netstat -tunapl 2>/dev/null | grep -c ESTABLISHED || echo "0") + local docker_count=$(docker ps -q 2>/dev/null | wc -l | tr -d ' ' || echo "0") + + cat > "$snapshot_file" << EOF +{ + "name": "$name", + "timestamp": "$(date -Iseconds)", + "epoch": $(date +%s), + "metrics": { + "cpu": $cpu, + "memory": $mem, + "disk": $disk, + "load": $load, + "connections": $connections, + "docker_containers": $docker_count + }, + "hostname": "$(hostname)", + "uptime": "$(uptime -p 2>/dev/null || uptime)" +} +EOF -# BlackRoad OS - Time Machine Dashboard -# Travel through your infrastructure's history + printf "${BR_GREEN}Snapshot saved: %s${RST}\n" "$name" +} -source ~/blackroad-dashboards/themes.sh -load_theme +list_snapshots() { + printf "${BOLD}Snapshots${RST}\n\n" + printf "%-30s %-20s %-10s %-10s\n" "NAME" "TIMESTAMP" "CPU" "MEM" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────────────────" + + for snapshot_file in "$SNAPSHOTS_DIR"/*.json; do + [[ ! -f "$snapshot_file" ]] && continue + if command -v jq &>/dev/null; then + local name=$(jq -r '.name' "$snapshot_file" 2>/dev/null) + local timestamp=$(jq -r '.timestamp' "$snapshot_file" 2>/dev/null) + local cpu=$(jq -r '.metrics.cpu' "$snapshot_file" 2>/dev/null) + local mem=$(jq -r '.metrics.memory' "$snapshot_file" 2>/dev/null) + printf "%-30s %-20s %8.1f%% %8.1f%%\n" "${name:0:30}" "${timestamp:0:20}" "$cpu" "$mem" + fi + done +} -HISTORY_FILE=~/blackroad-dashboards/.time_machine_history -CURRENT_TIME=$(date +%s) -TIME_POSITION=$CURRENT_TIME -PLAYBACK_SPEED=1 +compare_snapshots() { + local file1="$SNAPSHOTS_DIR/${1}.json" + local file2="$SNAPSHOTS_DIR/${2}.json" -# Initialize history -touch "$HISTORY_FILE" + [[ ! -f "$file1" || ! -f "$file2" ]] && { printf "${BR_RED}Snapshot not found${RST}\n"; return 1; } + + printf "${BOLD}Comparison: %s vs %s${RST}\n\n" "$1" "$2" + printf "%-15s %-15s %-15s %-10s\n" "METRIC" "$1" "$2" "CHANGE" + printf "${TEXT_MUTED}%s${RST}\n" "────────────────────────────────────────────────────" + + for metric in cpu memory disk load; do + local val1=$(jq -r ".metrics.$metric // 0" "$file1") + local val2=$(jq -r ".metrics.$metric // 0" "$file2") + local diff=$(echo "$val2 - $val1" | bc -l 2>/dev/null || echo "0") + printf "%-15s %-15s %-15s %+.2f\n" "$metric" "$val1" "$val2" "$diff" + done +} -# Add historical events -init_history() { - if [ ! -s "$HISTORY_FILE" ]; then - local now=$(date +%s) +#─────────────────────────────────────────────────────────────────────────────── +# RECORDING SYSTEM +#─────────────────────────────────────────────────────────────────────────────── - # Generate 30 days of history - for ((i=30; i>=0; i--)); do - local timestamp=$((now - i * 86400)) - local cpu=$((30 + RANDOM % 40)) - local memory=$((4 + RANDOM % 3)) - local containers=$((20 + RANDOM % 8)) +start_recording() { + local name="${1:-recording_$(date +%Y%m%d_%H%M%S)}" + local interval="${2:-5}" + local recording_file="$RECORDINGS_DIR/${name}.rec" - echo "$timestamp|cpu:$cpu|memory:${memory}.${RANDOM:0:1}|containers:$containers|deployments:$((RANDOM % 5))" >> "$HISTORY_FILE" + RECORDING_NAME="$name" + RECORDING_START=$(date +%s) + RECORDING_ACTIVE=true + + echo "# Recording: $name" > "$recording_file" + echo "# Started: $(date -Iseconds)" >> "$recording_file" + + touch "$recording_file.lock" + + ( + while [[ -f "$recording_file.lock" ]]; do + local ts=$(date +%s) + local cpu=$(grep 'cpu ' /proc/stat 2>/dev/null | awk '{usage=($2+$4)*100/($2+$4+$5)} END {printf "%.1f", usage}' || echo "0") + local mem=$(free 2>/dev/null | awk '/Mem:/ {printf "%.1f", $3/$2 * 100}' || echo "0") + local load=$(cat /proc/loadavg 2>/dev/null | awk '{print $1}' || echo "0") + echo "$ts|$cpu|$mem|$load" >> "$recording_file" + sleep "$interval" done - fi -} + ) & -# Format timestamp -format_time() { - local timestamp=$1 - date -r "$timestamp" "+%Y-%m-%d %H:%M:%S" + printf "${BR_GREEN}Recording started: %s${RST}\n" "$name" } -# Get data at specific time -get_historical_data() { - local target_time=$1 - - # Find closest historical record - local closest_line=$(awk -F'|' -v target="$target_time" ' - BEGIN { min_diff=999999999; closest="" } - { - diff = ($1 > target) ? $1 - target : target - $1 - if (diff < min_diff) { - min_diff = diff - closest = $0 - } - } - END { print closest } - ' "$HISTORY_FILE") - - echo "$closest_line" +stop_recording() { + local recording_file="$RECORDINGS_DIR/${RECORDING_NAME}.rec" + rm -f "$recording_file.lock" + RECORDING_ACTIVE=false + printf "${BR_GREEN}Recording stopped${RST}\n" } -# Show time machine dashboard -show_time_machine() { - clear - echo "" - echo -e "${BOLD}${GOLD}╔════════════════════════════════════════════════════════════════════════╗${RESET}" - echo -e "${BOLD}${GOLD}║${RESET} ${PURPLE}⏰${RESET} ${BOLD}TIME MACHINE DASHBOARD${RESET} ${BOLD}${GOLD}║${RESET}" - echo -e "${BOLD}${GOLD}╚════════════════════════════════════════════════════════════════════════╝${RESET}" - echo "" - - local current_data=$(get_historical_data "$TIME_POSITION") - local time_str=$(format_time "$TIME_POSITION") - - # Time indicator - echo -e "${TEXT_MUTED}╭─ TIME TRAVEL ─────────────────────────────────────────────────────────╮${RESET}" - echo "" - - local time_diff=$((CURRENT_TIME - TIME_POSITION)) - local days_ago=$((time_diff / 86400)) - - if [ $days_ago -eq 0 ]; then - echo -e " ${BOLD}${GREEN}◉ LIVE${RESET} ${CYAN}$time_str${RESET}" - else - echo -e " ${BOLD}${ORANGE}◉ $days_ago days ago${RESET} ${CYAN}$time_str${RESET}" - fi - echo "" - - # Timeline - echo -e "${TEXT_MUTED}╭─ TIMELINE ────────────────────────────────────────────────────────────╮${RESET}" - echo "" - - local total_days=30 - local position_days=$(( (CURRENT_TIME - TIME_POSITION) / 86400 )) - local bar_position=$((50 - position_days * 50 / total_days)) - - echo -n " ${TEXT_MUTED}30d ago${RESET} " - for ((i=0; i<50; i++)); do - if [ $i -eq $bar_position ]; then - echo -n "${GOLD}◆${RESET}" - else - echo -n "${TEXT_MUTED}─${RESET}" - fi +list_recordings() { + printf "${BOLD}Recordings${RST}\n\n" + for f in "$RECORDINGS_DIR"/*.rec; do + [[ -f "$f" ]] && printf " %s\n" "$(basename "$f" .rec)" done - echo " ${GREEN}NOW${RESET}" - echo "" - - # Historical metrics - echo -e "${TEXT_MUTED}╭─ METRICS AT THIS TIME ────────────────────────────────────────────────╮${RESET}" - echo "" - - if [ -n "$current_data" ]; then - local cpu=$(echo "$current_data" | grep -o 'cpu:[0-9]*' | cut -d: -f2) - local memory=$(echo "$current_data" | grep -o 'memory:[0-9.]*' | cut -d: -f2) - local containers=$(echo "$current_data" | grep -o 'containers:[0-9]*' | cut -d: -f2) - local deployments=$(echo "$current_data" | grep -o 'deployments:[0-9]*' | cut -d: -f2) - - echo -e " ${BOLD}${TEXT_PRIMARY}CPU Usage:${RESET}" - echo -n " ${ORANGE}" - for ((i=0; i<cpu/2; i++)); do echo -n "█"; done - echo -e "${TEXT_MUTED}$(for ((i=cpu/2; i<50; i++)); do echo -n "░"; done)${RESET} ${BOLD}${cpu}%${RESET}" - echo "" - - echo -e " ${BOLD}${TEXT_PRIMARY}Memory:${RESET} ${BOLD}${PINK}${memory} GB${RESET}" - echo -e " ${BOLD}${TEXT_PRIMARY}Containers:${RESET} ${BOLD}${CYAN}${containers}${RESET}" - echo -e " ${BOLD}${TEXT_PRIMARY}Deployments:${RESET} ${BOLD}${GREEN}${deployments}${RESET}" - else - echo -e " ${TEXT_MUTED}No data available at this time${RESET}" - fi - echo "" - - # Historical events - echo -e "${TEXT_MUTED}╭─ EVENTS AT THIS TIME ─────────────────────────────────────────────────╮${RESET}" - echo "" - - echo -e " ${GREEN}●${RESET} ${TEXT_MUTED}[$(format_time $((TIME_POSITION - 3600)))]${RESET} Deployment: ${CYAN}api-v2.3.1${RESET}" - echo -e " ${ORANGE}●${RESET} ${TEXT_MUTED}[$(format_time $((TIME_POSITION - 7200)))]${RESET} Alert: ${ORANGE}High CPU${RESET}" - echo -e " ${PURPLE}●${RESET} ${TEXT_MUTED}[$(format_time $((TIME_POSITION - 10800)))]${RESET} Scaled: ${PURPLE}+3 containers${RESET}" - echo -e " ${BLUE}●${RESET} ${TEXT_MUTED}[$(format_time $((TIME_POSITION - 14400)))]${RESET} Backup: ${BLUE}Completed${RESET}" - echo "" - - # Comparison with now - echo -e "${TEXT_MUTED}╭─ COMPARISON WITH NOW ─────────────────────────────────────────────────╮${RESET}" - echo "" - - local now_data=$(get_historical_data "$CURRENT_TIME") - local now_cpu=$(echo "$now_data" | grep -o 'cpu:[0-9]*' | cut -d: -f2) - local then_cpu=$(echo "$current_data" | grep -o 'cpu:[0-9]*' | cut -d: -f2) - - if [ -n "$then_cpu" ] && [ -n "$now_cpu" ]; then - local cpu_diff=$((now_cpu - then_cpu)) - - echo -e " ${BOLD}${TEXT_PRIMARY}CPU Change:${RESET}" - if [ $cpu_diff -gt 0 ]; then - echo -e " ${RED}↑ +${cpu_diff}%${RESET} ${TEXT_MUTED}(increased)${RESET}" - elif [ $cpu_diff -lt 0 ]; then - echo -e " ${GREEN}↓ ${cpu_diff}%${RESET} ${TEXT_MUTED}(decreased)${RESET}" - else - echo -e " ${CYAN}→ No change${RESET}" - fi - fi - echo "" - - # Time travel controls - echo -e "${TEXT_MUTED}╭─ TIME TRAVEL CONTROLS ────────────────────────────────────────────────╮${RESET}" - echo "" - echo -e " ${CYAN}←${RESET} ${BOLD}1 Hour Back${RESET} ${PURPLE}→${RESET} ${BOLD}1 Hour Forward${RESET}" - echo -e " ${ORANGE}[${RESET} ${BOLD}1 Day Back${RESET} ${PINK}]${RESET} ${BOLD}1 Day Forward${RESET}" - echo -e " ${GREEN}H${RESET} ${BOLD}Go to Start${RESET} ${GOLD}N${RESET} ${BOLD}Go to Now${RESET}" - echo "" - echo -e " ${BOLD}${TEXT_PRIMARY}Playback Speed:${RESET} ${CYAN}${PLAYBACK_SPEED}x${RESET}" - echo "" - - # Snapshots - echo -e "${TEXT_MUTED}╭─ SAVED SNAPSHOTS ─────────────────────────────────────────────────────╮${RESET}" - echo "" - echo -e " ${PURPLE}📸${RESET} ${BOLD}Pre-Deployment${RESET} ${TEXT_MUTED}2024-12-20 15:30${RESET} ${GREEN}✓${RESET}" - echo -e " ${CYAN}📸${RESET} ${BOLD}After Scaling${RESET} ${TEXT_MUTED}2024-12-18 09:15${RESET} ${GREEN}✓${RESET}" - echo -e " ${ORANGE}📸${RESET} ${BOLD}Peak Traffic${RESET} ${TEXT_MUTED}2024-12-15 14:45${RESET} ${GREEN}✓${RESET}" - echo "" - - # Historical chart - echo -e "${TEXT_MUTED}╭─ CPU HISTORY (30 DAYS) ───────────────────────────────────────────────╮${RESET}" - echo "" - - echo " ${TEXT_MUTED}%${RESET}" - echo " ${TEXT_MUTED}100${RESET} ${RED}│${RESET}" - echo " ${TEXT_MUTED}75${RESET} ${ORANGE}│${RESET} ${ORANGE}▄█▄${RESET}" - echo " ${TEXT_MUTED}50${RESET} ${YELLOW}│${RESET} ${YELLOW}▄█${RESET}${ORANGE}█${RESET}${YELLOW}█▄${RESET} ${YELLOW}▄${RESET} ${GOLD}◆${RESET} ${TEXT_MUTED}You are here${RESET}" - echo " ${TEXT_MUTED}25${RESET} ${GREEN}│${RESET}${GREEN}▄██${RESET}${YELLOW}█${RESET} ${YELLOW}█${RESET}${GREEN}██▄${RESET}" - echo " ${TEXT_MUTED}0${RESET} ${TEXT_MUTED}└────────────────────────────────────────────────→${RESET}" - echo " ${TEXT_MUTED}30d 25d 20d 15d 10d 5d NOW${RESET}" - echo "" - - echo -e "${GOLD}─────────────────────────────────────────────────────────────────────────${RESET}" - echo -e " ${TEXT_SECONDARY}[←/→]${RESET} Hours ${TEXT_SECONDARY}[[/]]${RESET} Days ${TEXT_SECONDARY}[H]${RESET} Start ${TEXT_SECONDARY}[N]${RESET} Now ${TEXT_SECONDARY}[Q]${RESET} Quit" - echo "" } -# Main loop -main() { - init_history +#─────────────────────────────────────────────────────────────────────────────── +# REPLAY SYSTEM +#─────────────────────────────────────────────────────────────────────────────── - while true; do - show_time_machine +replay_recording() { + local name="$1" + local speed="${2:-1}" + local recording_file="$RECORDINGS_DIR/${name}.rec" - read -rsn1 key + [[ ! -f "$recording_file" ]] && { printf "${BR_RED}Recording not found${RST}\n"; return 1; } - # Handle escape sequences for arrow keys - if [[ $key == $'\x1b' ]]; then - read -rsn2 key - fi + clear_screen + cursor_hide - case "$key" in - '[D'|'h'|'H') # Left arrow or H - if [ "$key" = "h" ] || [ "$key" = "H" ]; then - # Go to start - TIME_POSITION=$((CURRENT_TIME - 30 * 86400)) - else - # 1 hour back - TIME_POSITION=$((TIME_POSITION - 3600)) - fi - ;; - '[C'|'n'|'N') # Right arrow or N - if [ "$key" = "n" ] || [ "$key" = "N" ]; then - # Go to now - TIME_POSITION=$CURRENT_TIME - else - # 1 hour forward - TIME_POSITION=$((TIME_POSITION + 3600)) - [ $TIME_POSITION -gt $CURRENT_TIME ] && TIME_POSITION=$CURRENT_TIME - fi - ;; - '[') - # 1 day back - TIME_POSITION=$((TIME_POSITION - 86400)) - ;; - ']') - # 1 day forward - TIME_POSITION=$((TIME_POSITION + 86400)) - [ $TIME_POSITION -gt $CURRENT_TIME ] && TIME_POSITION=$CURRENT_TIME - ;; - 'q'|'Q') - echo -e "\n${CYAN}Time travel ended${RESET}\n" - exit 0 - ;; - esac + printf "${BR_PURPLE}${BOLD}Replaying: %s (Speed: %.1fx)${RST}\n\n" "$name" "$speed" + + local idx=0 + while IFS='|' read -r ts cpu mem load; do + [[ ! "$ts" =~ ^[0-9]+$ ]] && continue + ((idx++)) + + printf "\r Time: %s CPU: %6.1f%% MEM: %6.1f%% Load: %s " \ + "$(date -d "@$ts" '+%H:%M:%S' 2>/dev/null || echo "$ts")" "$cpu" "$mem" "$load" + + local delay=$(echo "1 / $speed" | bc -l 2>/dev/null || echo 1) + sleep "$delay" + done < "$recording_file" + + printf "\n\n${BR_GREEN}Replay complete! (%d points)${RST}\n" "$idx" + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# EVENT LOGGING +#─────────────────────────────────────────────────────────────────────────────── + +log_event() { + echo "$(date -Iseconds)|$1|$2|$3" >> "$EVENTS_FILE" +} + +show_events() { + printf "${BOLD}Recent Events${RST}\n\n" + tail -n "${1:-20}" "$EVENTS_FILE" 2>/dev/null | while IFS='|' read -r ts type source msg; do + printf "%-25s %-10s %-15s %s\n" "${ts:0:25}" "$type" "$source" "$msg" + done +} - # Keep within bounds - local min_time=$((CURRENT_TIME - 30 * 86400)) - [ $TIME_POSITION -lt $min_time ] && TIME_POSITION=$min_time +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +time_machine_dashboard() { + clear_screen + cursor_hide + + while true; do + cursor_to 1 1 + + printf "${BR_PURPLE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ ⏱️ TIME MACHINE ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + local snap_count=$(ls "$SNAPSHOTS_DIR"/*.json 2>/dev/null | wc -l) + local rec_count=$(ls "$RECORDINGS_DIR"/*.rec 2>/dev/null | wc -l) + + printf " ${BOLD}Snapshots:${RST} ${BR_CYAN}%s${RST} " "$snap_count" + printf "${BOLD}Recordings:${RST} ${BR_YELLOW}%s${RST}\n\n" "$rec_count" + + [[ "$RECORDING_ACTIVE" == "true" ]] && \ + printf " ${BR_RED}● RECORDING:${RST} %s\n\n" "$RECORDING_NAME" + + printf "${BOLD}Options:${RST}\n" + printf " ${BR_CYAN}1.${RST} Take Snapshot ${BR_CYAN}4.${RST} List Snapshots\n" + printf " ${BR_CYAN}2.${RST} Start Recording ${BR_CYAN}5.${RST} List Recordings\n" + printf " ${BR_CYAN}3.${RST} Stop Recording ${BR_CYAN}6.${RST} Replay Recording\n" + printf " ${BR_CYAN}7.${RST} Compare ${BR_CYAN}8.${RST} Events\n" + printf " ${TEXT_MUTED}Q.${RST} Quit\n\n" + + read -rsn1 choice + case "$choice" in + 1) printf "\n${BR_CYAN}Name: ${RST}"; read -r n; take_snapshot "$n"; sleep 1 ;; + 2) printf "\n${BR_CYAN}Name: ${RST}"; read -r n; start_recording "$n"; sleep 1 ;; + 3) stop_recording; sleep 1 ;; + 4) clear_screen; list_snapshots; read -rsn1 ;; + 5) clear_screen; list_recordings; read -rsn1 ;; + 6) printf "\n${BR_CYAN}Name: ${RST}"; read -r n; replay_recording "$n" ;; + 7) printf "\n${BR_CYAN}Snap 1: ${RST}"; read -r s1; printf "${BR_CYAN}Snap 2: ${RST}"; read -r s2; compare_snapshots "$s1" "$s2"; read -rsn1 ;; + 8) clear_screen; show_events 30; read -rsn1 ;; + q|Q) break ;; + esac + clear_screen done + cursor_show } -# Run -main +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) time_machine_dashboard ;; + snapshot) take_snapshot "$2" ;; + snapshots) list_snapshots ;; + compare) compare_snapshots "$2" "$3" ;; + record) start_recording "$2" "$3" ;; + stop) stop_recording ;; + recordings) list_recordings ;; + replay) replay_recording "$2" "$3" ;; + events) show_events "$2" ;; + log) log_event "$2" "$3" "$4" ;; + *) printf "Usage: %s [dashboard|snapshot|record|replay|...]\n" "$0" ;; + esac +fi diff --git a/weather-dashboard.sh b/weather-dashboard.sh new file mode 100644 index 0000000..81e9535 --- /dev/null +++ b/weather-dashboard.sh @@ -0,0 +1,532 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██╗███████╗ █████╗ ████████╗██╗ ██╗███████╗██████╗ +# ██║ ██║██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██╔══██╗ +# ██║ █╗ ██║█████╗ ███████║ ██║ ███████║█████╗ ██████╔╝ +# ██║███╗██║██╔══╝ ██╔══██║ ██║ ██╔══██║██╔══╝ ██╔══██╗ +# ╚███╔███╔╝███████╗██║ ██║ ██║ ██║ ██║███████╗██║ ██║ +# ╚══╝╚══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD WEATHER DASHBOARD v3.0 +# Weather Forecasts with ASCII Art & Animations +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +CACHE_DIR="${BLACKROAD_HOME:-$HOME/.blackroad-dashboards}/cache/weather" +mkdir -p "$CACHE_DIR" 2>/dev/null + +DEFAULT_CITY="${WEATHER_CITY:-San Francisco}" +UNITS="${WEATHER_UNITS:-metric}" # metric or imperial + +# Open-Meteo API (free, no key required) +API_BASE="https://api.open-meteo.com/v1" + +#─────────────────────────────────────────────────────────────────────────────── +# WEATHER ASCII ART +#─────────────────────────────────────────────────────────────────────────────── + +render_sun() { + printf "\033[38;5;226m" + cat << 'EOF' + \ / + .-. + ― ( ) ― + `-' + / \ +EOF + printf "\033[0m" +} + +render_cloud() { + printf "\033[38;5;250m" + cat << 'EOF' + .--. + .-( ). + (___.__)__) +EOF + printf "\033[0m" +} + +render_partly_cloudy() { + printf "\033[38;5;226m \\ \033[38;5;250m .--. \n" + printf "\033[38;5;226m .-\033[38;5;250m.-( ). \n" + printf "\033[38;5;226m ― (\033[38;5;250m___.__)__)\n" + printf "\033[38;5;226m '-' \n" + printf "\033[0m" +} + +render_rain() { + printf "\033[38;5;250m" + cat << 'EOF' + .-. + ( ). + (___(__) + ‚ʻ‚ʻ‚ʻ‚ʻ + ‚ʻ‚ʻ‚ʻ‚ʻ +EOF + printf "\033[0m" +} + +render_heavy_rain() { + printf "\033[38;5;240m" + cat << 'EOF' + .-. + ( ). + (___(__) + ‚'‚'‚'‚'‚' + ‚'‚'‚'‚'‚' + ‚'‚'‚'‚'‚' +EOF + printf "\033[0m" +} + +render_snow() { + printf "\033[38;5;255m" + cat << 'EOF' + .-. + ( ). + (___(__) + * * * * + * * * * +EOF + printf "\033[0m" +} + +render_thunderstorm() { + printf "\033[38;5;240m" + cat << 'EOF' + .-. + ( ). + (___(__) +EOF + printf "\033[38;5;226m" + cat << 'EOF' + ⚡‚'⚡‚' + ‚'‚'‚'‚' +EOF + printf "\033[0m" +} + +render_fog() { + printf "\033[38;5;250m" + cat << 'EOF' + _ - _ - _ - + _ - _ - _ + _ - _ - _ - + _ - _ - _ +EOF + printf "\033[0m" +} + +render_clear_night() { + printf "\033[38;5;229m" + cat << 'EOF' + .--. + ( ) + ( ) + ( ) + '--' +EOF + printf "\033[0m" +} + +render_weather_icon() { + local code="$1" + local is_day="${2:-1}" + + case "$code" in + 0) # Clear + [[ "$is_day" == "1" ]] && render_sun || render_clear_night + ;; + 1|2) # Partly cloudy + render_partly_cloudy + ;; + 3) # Overcast + render_cloud + ;; + 45|48) # Fog + render_fog + ;; + 51|53|55|61|63|80|81) # Rain + render_rain + ;; + 65|82) # Heavy rain + render_heavy_rain + ;; + 71|73|75|77|85|86) # Snow + render_snow + ;; + 95|96|99) # Thunderstorm + render_thunderstorm + ;; + *) + render_cloud + ;; + esac +} + +get_weather_description() { + local code="$1" + + case "$code" in + 0) echo "Clear sky" ;; + 1) echo "Mainly clear" ;; + 2) echo "Partly cloudy" ;; + 3) echo "Overcast" ;; + 45) echo "Fog" ;; + 48) echo "Depositing rime fog" ;; + 51) echo "Light drizzle" ;; + 53) echo "Moderate drizzle" ;; + 55) echo "Dense drizzle" ;; + 61) echo "Slight rain" ;; + 63) echo "Moderate rain" ;; + 65) echo "Heavy rain" ;; + 71) echo "Slight snow" ;; + 73) echo "Moderate snow" ;; + 75) echo "Heavy snow" ;; + 77) echo "Snow grains" ;; + 80) echo "Slight showers" ;; + 81) echo "Moderate showers" ;; + 82) echo "Violent showers" ;; + 85) echo "Slight snow showers" ;; + 86) echo "Heavy snow showers" ;; + 95) echo "Thunderstorm" ;; + 96) echo "Thunderstorm with hail" ;; + 99) echo "Severe thunderstorm" ;; + *) echo "Unknown" ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# GEOCODING +#─────────────────────────────────────────────────────────────────────────────── + +geocode_city() { + local city="$1" + local cache_file="$CACHE_DIR/geo_$(echo "$city" | tr ' ' '_').json" + + # Check cache + if [[ -f "$cache_file" ]]; then + local age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0))) + if [[ $age -lt 86400 ]]; then # 24 hour cache + cat "$cache_file" + return + fi + fi + + local result=$(curl -s "https://geocoding-api.open-meteo.com/v1/search?name=${city// /%20}&count=1" 2>/dev/null) + + if [[ -n "$result" ]]; then + echo "$result" > "$cache_file" + echo "$result" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# WEATHER DATA +#─────────────────────────────────────────────────────────────────────────────── + +fetch_weather() { + local lat="$1" + local lon="$2" + local cache_file="$CACHE_DIR/weather_${lat}_${lon}.json" + + # Check cache (15 min) + if [[ -f "$cache_file" ]]; then + local age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0))) + if [[ $age -lt 900 ]]; then + cat "$cache_file" + return + fi + fi + + local temp_unit="celsius" + local wind_unit="kmh" + [[ "$UNITS" == "imperial" ]] && temp_unit="fahrenheit" && wind_unit="mph" + + local url="$API_BASE/forecast?latitude=$lat&longitude=$lon" + url+="¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,wind_speed_10m,wind_direction_10m" + url+="&hourly=temperature_2m,precipitation_probability,weather_code" + url+="&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum,precipitation_probability_max" + url+="&temperature_unit=$temp_unit&wind_speed_unit=$wind_unit&timezone=auto&forecast_days=7" + + local result=$(curl -s "$url" 2>/dev/null) + + if [[ -n "$result" ]]; then + echo "$result" > "$cache_file" + echo "$result" + fi +} + +#─────────────────────────────────────────────────────────────────────────────── +# RENDERING +#─────────────────────────────────────────────────────────────────────────────── + +render_current_weather() { + local weather="$1" + local city="$2" + + if ! command -v jq &>/dev/null; then + printf "\033[38;5;196mjq required for weather parsing\033[0m\n" + return + fi + + local temp=$(echo "$weather" | jq -r '.current.temperature_2m // "N/A"') + local feels_like=$(echo "$weather" | jq -r '.current.apparent_temperature // "N/A"') + local humidity=$(echo "$weather" | jq -r '.current.relative_humidity_2m // "N/A"') + local wind=$(echo "$weather" | jq -r '.current.wind_speed_10m // "N/A"') + local code=$(echo "$weather" | jq -r '.current.weather_code // 0') + local is_day=$(echo "$weather" | jq -r '.current.is_day // 1') + + local temp_unit="°C" + local wind_unit="km/h" + [[ "$UNITS" == "imperial" ]] && temp_unit="°F" && wind_unit="mph" + + local description=$(get_weather_description "$code") + + # Header + printf "\n \033[1;38;5;51m%s\033[0m\n" "$city" + printf " \033[38;5;240m%s\033[0m\n\n" "$(date '+%A, %B %d %Y')" + + # Weather icon (side by side with data) + local icon_output=$(render_weather_icon "$code" "$is_day") + local -a icon_lines + mapfile -t icon_lines <<< "$icon_output" + + local data_lines=( + "$(printf "\033[1;38;5;226m%.0f%s\033[0m %s" "$temp" "$temp_unit" "$description")" + "$(printf "\033[38;5;245mFeels like: %.0f%s\033[0m" "$feels_like" "$temp_unit")" + "$(printf "\033[38;5;39mHumidity: %s%%\033[0m" "$humidity")" + "$(printf "\033[38;5;51mWind: %s %s\033[0m" "$wind" "$wind_unit")" + ) + + local max_lines=${#icon_lines[@]} + [[ ${#data_lines[@]} -gt $max_lines ]] && max_lines=${#data_lines[@]} + + for ((i=0; i<max_lines; i++)); do + printf " %-20s %s\n" "${icon_lines[$i]:-}" "${data_lines[$i]:-}" + done +} + +render_hourly_forecast() { + local weather="$1" + local hours="${2:-12}" + + printf "\n \033[1mHourly Forecast\033[0m\n\n" + + local temp_unit="°C" + [[ "$UNITS" == "imperial" ]] && temp_unit="°F" + + # Current hour index + local current_hour=$(date +%H) + local -a times=($(echo "$weather" | jq -r '.hourly.time[]' 2>/dev/null)) + local -a temps=($(echo "$weather" | jq -r '.hourly.temperature_2m[]' 2>/dev/null)) + local -a probs=($(echo "$weather" | jq -r '.hourly.precipitation_probability[]' 2>/dev/null)) + local -a codes=($(echo "$weather" | jq -r '.hourly.weather_code[]' 2>/dev/null)) + + # Find current hour index + local start_idx=0 + for ((i=0; i<${#times[@]}; i++)); do + if [[ "${times[$i]}" == *"T${current_hour}:"* ]]; then + start_idx=$i + break + fi + done + + printf " " + for ((i=start_idx; i<start_idx+hours && i<${#times[@]}; i++)); do + local hour=$(echo "${times[$i]}" | grep -oP 'T\K\d+') + printf "\033[38;5;245m%3s\033[0m " "$hour" + done + printf "\n " + + for ((i=start_idx; i<start_idx+hours && i<${#times[@]}; i++)); do + local temp="${temps[$i]}" + local color="\033[38;5;39m" + [[ ${temp%.*} -gt 25 ]] && color="\033[38;5;226m" + [[ ${temp%.*} -gt 30 ]] && color="\033[38;5;196m" + [[ ${temp%.*} -lt 10 ]] && color="\033[38;5;51m" + printf "%s%3.0f\033[0m " "$color" "$temp" + done + printf "\n " + + for ((i=start_idx; i<start_idx+hours && i<${#times[@]}; i++)); do + local prob="${probs[$i]}" + if [[ ${prob%.*} -gt 50 ]]; then + printf "\033[38;5;39m%3d%%\033[0m" "$prob" + else + printf "\033[38;5;240m%3d%%\033[0m" "$prob" + fi + done + printf "\n" +} + +render_daily_forecast() { + local weather="$1" + + printf "\n \033[1m7-Day Forecast\033[0m\n\n" + + local temp_unit="°" + local days=($(echo "$weather" | jq -r '.daily.time[]' 2>/dev/null)) + local codes=($(echo "$weather" | jq -r '.daily.weather_code[]' 2>/dev/null)) + local maxs=($(echo "$weather" | jq -r '.daily.temperature_2m_max[]' 2>/dev/null)) + local mins=($(echo "$weather" | jq -r '.daily.temperature_2m_min[]' 2>/dev/null)) + local probs=($(echo "$weather" | jq -r '.daily.precipitation_probability_max[]' 2>/dev/null)) + + for ((i=0; i<${#days[@]} && i<7; i++)); do + local day_name=$(date -d "${days[$i]}" '+%a' 2>/dev/null || echo "${days[$i]}") + local code="${codes[$i]}" + local max="${maxs[$i]}" + local min="${mins[$i]}" + local prob="${probs[$i]:-0}" + local desc=$(get_weather_description "$code") + + # Mini weather icon + local icon="☀" + case "$code" in + 0) icon="☀" ;; + 1|2) icon="⛅" ;; + 3) icon="☁" ;; + 45|48) icon="🌫" ;; + 51|53|55|61|63|80|81) icon="🌧" ;; + 65|82) icon="🌧" ;; + 71|73|75|77|85|86) icon="❄" ;; + 95|96|99) icon="⛈" ;; + esac + + printf " \033[1m%-3s\033[0m %s " "$day_name" "$icon" + + # Temperature bar + local range=$((${max%.*} - ${min%.*})) + local bar_len=$((range / 2)) + [[ $bar_len -lt 1 ]] && bar_len=1 + [[ $bar_len -gt 15 ]] && bar_len=15 + + printf "\033[38;5;39m%2.0f$temp_unit\033[0m " "$min" + printf "\033[38;5;214m" + printf "%0.s▓" $(seq 1 $bar_len) + printf "\033[0m" + printf " \033[38;5;196m%2.0f$temp_unit\033[0m " "$max" + + [[ ${prob%.*} -gt 30 ]] && printf "\033[38;5;39m💧%d%%\033[0m" "$prob" + printf "\n" + done +} + +render_sun_times() { + local weather="$1" + + local sunrise=$(echo "$weather" | jq -r '.daily.sunrise[0]' 2>/dev/null) + local sunset=$(echo "$weather" | jq -r '.daily.sunset[0]' 2>/dev/null) + + sunrise=$(echo "$sunrise" | grep -oP 'T\K\d+:\d+') + sunset=$(echo "$sunset" | grep -oP 'T\K\d+:\d+') + + printf "\n \033[38;5;226m☀ Sunrise: %s\033[0m " "$sunrise" + printf "\033[38;5;208m☀ Sunset: %s\033[0m\n" "$sunset" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN DASHBOARD +#─────────────────────────────────────────────────────────────────────────────── + +weather_dashboard() { + local city="${1:-$DEFAULT_CITY}" + + clear + + printf "\033[1;38;5;39m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🌤️ BLACKROAD WEATHER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m" + + printf "\n \033[38;5;245mFetching weather for %s...\033[0m\n" "$city" + + # Geocode city + local geo=$(geocode_city "$city") + + if ! command -v jq &>/dev/null; then + printf "\n \033[38;5;196mjq required for weather data\033[0m\n" + return 1 + fi + + local lat=$(echo "$geo" | jq -r '.results[0].latitude // empty') + local lon=$(echo "$geo" | jq -r '.results[0].longitude // empty') + local name=$(echo "$geo" | jq -r '.results[0].name // empty') + local country=$(echo "$geo" | jq -r '.results[0].country // empty') + + if [[ -z "$lat" || -z "$lon" ]]; then + printf "\n \033[38;5;196mCity not found: %s\033[0m\n" "$city" + return 1 + fi + + local full_name="$name, $country" + + # Fetch weather + local weather=$(fetch_weather "$lat" "$lon") + + if [[ -z "$weather" ]]; then + printf "\n \033[38;5;196mFailed to fetch weather data\033[0m\n" + return 1 + fi + + # Render + clear + printf "\033[1;38;5;39m" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ 🌤️ BLACKROAD WEATHER ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "\033[0m" + + render_current_weather "$weather" "$full_name" + render_sun_times "$weather" + render_hourly_forecast "$weather" 10 + render_daily_forecast "$weather" + + printf "\n\033[38;5;240m───────────────────────────────────────────────────────────────────────────────\033[0m\n" + printf " \033[38;5;245m[R] Refresh [C] Change city [U] Toggle units [Q] Quit\033[0m\n" +} + +weather_dashboard_loop() { + local city="${1:-$DEFAULT_CITY}" + + while true; do + weather_dashboard "$city" + + if read -rsn1 -t 300 key 2>/dev/null; then + case "$key" in + r|R) continue ;; + c|C) + printf "\n \033[38;5;51mEnter city: \033[0m" + read -r new_city + [[ -n "$new_city" ]] && city="$new_city" + ;; + u|U) + [[ "$UNITS" == "metric" ]] && UNITS="imperial" || UNITS="metric" + ;; + q|Q) break ;; + esac + fi + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-dashboard}" in + dashboard) weather_dashboard_loop "$2" ;; + current) weather_dashboard "$2" ;; + *) + printf "Usage: %s [dashboard|current] [city]\n" "$0" + ;; + esac +fi diff --git a/workflow-engine.sh b/workflow-engine.sh new file mode 100644 index 0000000..11c9b89 --- /dev/null +++ b/workflow-engine.sh @@ -0,0 +1,636 @@ +#!/bin/bash +#═══════════════════════════════════════════════════════════════════════════════ +# ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗ +# ██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝██╔════╝██║ ██╔═══██╗██║ ██║ +# ██║ █╗ ██║██║ ██║██████╔╝█████╔╝ █████╗ ██║ ██║ ██║██║ █╗ ██║ +# ██║███╗██║██║ ██║██╔══██╗██╔═██╗ ██╔══╝ ██║ ██║ ██║██║███╗██║ +# ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗██║ ███████╗╚██████╔╝╚███╔███╔╝ +# ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝ +#═══════════════════════════════════════════════════════════════════════════════ +# BLACKROAD WORKFLOW AUTOMATION ENGINE v3.0 +# Visual Workflows, Triggers, Conditions, Actions, Schedules +#═══════════════════════════════════════════════════════════════════════════════ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +[[ -f "$SCRIPT_DIR/lib-core.sh" ]] && source "$SCRIPT_DIR/lib-core.sh" + +#─────────────────────────────────────────────────────────────────────────────── +# CONFIGURATION +#─────────────────────────────────────────────────────────────────────────────── + +WORKFLOW_DIR="${BLACKROAD_DATA:-$HOME/.blackroad-dashboards}/workflows" +WORKFLOW_LOGS="$WORKFLOW_DIR/logs" +WORKFLOW_STATE="$WORKFLOW_DIR/state" +mkdir -p "$WORKFLOW_DIR" "$WORKFLOW_LOGS" "$WORKFLOW_STATE" 2>/dev/null + +# Workflow registry +declare -A WORKFLOWS +declare -A WORKFLOW_STATUS +declare -A WORKFLOW_LAST_RUN + +# Execution tracking +declare -a EXECUTION_QUEUE +EXECUTION_RUNNING=0 + +#─────────────────────────────────────────────────────────────────────────────── +# WORKFLOW DEFINITION +#─────────────────────────────────────────────────────────────────────────────── + +# Create a new workflow +workflow_create() { + local name="$1" + local description="${2:-}" + local workflow_file="$WORKFLOW_DIR/${name}.workflow" + + cat > "$workflow_file" << EOF +{ + "name": "$name", + "description": "$description", + "version": "1.0.0", + "enabled": true, + "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "triggers": [], + "conditions": [], + "actions": [], + "on_success": [], + "on_failure": [], + "settings": { + "timeout": 300, + "retry_count": 3, + "retry_delay": 10, + "parallel": false, + "log_level": "info" + } +} +EOF + + WORKFLOWS[$name]="$workflow_file" + log_info "Workflow created: $name" + echo "$workflow_file" +} + +# Load workflow from file +workflow_load() { + local workflow_file="$1" + + [[ ! -f "$workflow_file" ]] && return 1 + + if command -v jq &>/dev/null; then + local name=$(jq -r '.name' "$workflow_file" 2>/dev/null) + [[ -n "$name" ]] && WORKFLOWS[$name]="$workflow_file" + fi +} + +# Load all workflows +workflow_load_all() { + for file in "$WORKFLOW_DIR"/*.workflow; do + [[ -f "$file" ]] && workflow_load "$file" + done +} + +#─────────────────────────────────────────────────────────────────────────────── +# TRIGGERS +#─────────────────────────────────────────────────────────────────────────────── + +# Available trigger types +declare -A TRIGGER_TYPES=( + [schedule]="Run on schedule (cron expression)" + [webhook]="HTTP webhook trigger" + [file_change]="File system changes" + [metric_threshold]="Metric exceeds threshold" + [event]="Custom event trigger" + [startup]="Run on system startup" + [manual]="Manual trigger only" +) + +# Add trigger to workflow +workflow_add_trigger() { + local workflow_name="$1" + local trigger_type="$2" + shift 2 + local trigger_config="$*" + + local workflow_file="${WORKFLOWS[$workflow_name]}" + [[ ! -f "$workflow_file" ]] && return 1 + + local trigger_json=$(cat << EOF +{ + "type": "$trigger_type", + "config": $trigger_config, + "enabled": true, + "id": "trigger_$(date +%s)" +} +EOF +) + + if command -v jq &>/dev/null; then + jq ".triggers += [$trigger_json]" "$workflow_file" > "${workflow_file}.tmp" + mv "${workflow_file}.tmp" "$workflow_file" + fi +} + +# Check if trigger should fire +trigger_check() { + local trigger_type="$1" + local trigger_config="$2" + + case "$trigger_type" in + schedule) + # Parse cron expression and check + local cron_expr="$trigger_config" + # Simplified: check if current time matches (implement full cron later) + return 0 + ;; + metric_threshold) + local metric=$(echo "$trigger_config" | jq -r '.metric' 2>/dev/null) + local operator=$(echo "$trigger_config" | jq -r '.operator' 2>/dev/null) + local value=$(echo "$trigger_config" | jq -r '.value' 2>/dev/null) + + local current + case "$metric" in + cpu) current=$(get_cpu_usage 2>/dev/null || echo "0") ;; + memory) current=$(get_memory_usage 2>/dev/null || echo "0") ;; + disk) current=$(get_disk_usage "/" 2>/dev/null || echo "0") ;; + esac + + case "$operator" in + ">"|"gt") [[ $current -gt $value ]] && return 0 ;; + "<"|"lt") [[ $current -lt $value ]] && return 0 ;; + ">="|"gte") [[ $current -ge $value ]] && return 0 ;; + "<="|"lte") [[ $current -le $value ]] && return 0 ;; + "="|"eq") [[ $current -eq $value ]] && return 0 ;; + esac + return 1 + ;; + manual) + return 1 # Never auto-trigger + ;; + *) + return 1 + ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# CONDITIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Add condition to workflow +workflow_add_condition() { + local workflow_name="$1" + local condition_type="$2" + local condition_config="$3" + + local workflow_file="${WORKFLOWS[$workflow_name]}" + [[ ! -f "$workflow_file" ]] && return 1 + + local condition_json=$(cat << EOF +{ + "type": "$condition_type", + "config": $condition_config, + "id": "cond_$(date +%s)" +} +EOF +) + + if command -v jq &>/dev/null; then + jq ".conditions += [$condition_json]" "$workflow_file" > "${workflow_file}.tmp" + mv "${workflow_file}.tmp" "$workflow_file" + fi +} + +# Evaluate condition +condition_evaluate() { + local condition_type="$1" + local condition_config="$2" + + case "$condition_type" in + time_window) + local start_hour=$(echo "$condition_config" | jq -r '.start_hour' 2>/dev/null) + local end_hour=$(echo "$condition_config" | jq -r '.end_hour' 2>/dev/null) + local current_hour=$(date +%H) + + [[ $current_hour -ge $start_hour && $current_hour -lt $end_hour ]] && return 0 + return 1 + ;; + weekday) + local allowed_days=$(echo "$condition_config" | jq -r '.days[]' 2>/dev/null) + local current_day=$(date +%u) # 1-7, Monday is 1 + + echo "$allowed_days" | grep -q "$current_day" && return 0 + return 1 + ;; + host_alive) + local host=$(echo "$condition_config" | jq -r '.host' 2>/dev/null) + ping -c 1 -W 2 "$host" &>/dev/null && return 0 + return 1 + ;; + file_exists) + local path=$(echo "$condition_config" | jq -r '.path' 2>/dev/null) + [[ -f "$path" ]] && return 0 + return 1 + ;; + always) + return 0 + ;; + *) + return 0 + ;; + esac +} + +#─────────────────────────────────────────────────────────────────────────────── +# ACTIONS +#─────────────────────────────────────────────────────────────────────────────── + +# Available action types +declare -A ACTION_TYPES=( + [shell]="Execute shell command" + [script]="Run script file" + [notify]="Send notification" + [http]="HTTP request" + [ssh]="Remote SSH command" + [email]="Send email" + [slack]="Send Slack message" + [wait]="Wait/delay" + [log]="Log message" + [export]="Export data" + [restart_service]="Restart a service" + [scale]="Scale resources" +) + +# Add action to workflow +workflow_add_action() { + local workflow_name="$1" + local action_type="$2" + local action_config="$3" + + local workflow_file="${WORKFLOWS[$workflow_name]}" + [[ ! -f "$workflow_file" ]] && return 1 + + local action_json=$(cat << EOF +{ + "type": "$action_type", + "config": $action_config, + "id": "action_$(date +%s)", + "timeout": 60 +} +EOF +) + + if command -v jq &>/dev/null; then + jq ".actions += [$action_json]" "$workflow_file" > "${workflow_file}.tmp" + mv "${workflow_file}.tmp" "$workflow_file" + fi +} + +# Execute action +action_execute() { + local action_type="$1" + local action_config="$2" + local workflow_context="$3" + + local start_time=$(date +%s) + local result="" + local exit_code=0 + + case "$action_type" in + shell) + local command=$(echo "$action_config" | jq -r '.command' 2>/dev/null) + result=$(eval "$command" 2>&1) || exit_code=$? + ;; + script) + local script_path=$(echo "$action_config" | jq -r '.path' 2>/dev/null) + local args=$(echo "$action_config" | jq -r '.args // ""' 2>/dev/null) + result=$(bash "$script_path" $args 2>&1) || exit_code=$? + ;; + notify) + local level=$(echo "$action_config" | jq -r '.level // "info"' 2>/dev/null) + local title=$(echo "$action_config" | jq -r '.title' 2>/dev/null) + local message=$(echo "$action_config" | jq -r '.message' 2>/dev/null) + + if [[ -f "$SCRIPT_DIR/notification-system.sh" ]]; then + source "$SCRIPT_DIR/notification-system.sh" + notify "$level" "$title" "$message" + fi + result="Notification sent" + ;; + http) + local url=$(echo "$action_config" | jq -r '.url' 2>/dev/null) + local method=$(echo "$action_config" | jq -r '.method // "GET"' 2>/dev/null) + local body=$(echo "$action_config" | jq -r '.body // ""' 2>/dev/null) + + case "$method" in + GET) result=$(curl -s "$url" 2>&1) || exit_code=$? ;; + POST) result=$(curl -s -X POST -d "$body" "$url" 2>&1) || exit_code=$? ;; + esac + ;; + ssh) + local host=$(echo "$action_config" | jq -r '.host' 2>/dev/null) + local command=$(echo "$action_config" | jq -r '.command' 2>/dev/null) + result=$(ssh -o ConnectTimeout=10 "$host" "$command" 2>&1) || exit_code=$? + ;; + wait) + local seconds=$(echo "$action_config" | jq -r '.seconds // 1' 2>/dev/null) + sleep "$seconds" + result="Waited ${seconds}s" + ;; + log) + local message=$(echo "$action_config" | jq -r '.message' 2>/dev/null) + local level=$(echo "$action_config" | jq -r '.level // "info"' 2>/dev/null) + log "$level" "$message" + result="Logged" + ;; + export) + local format=$(echo "$action_config" | jq -r '.format // "json"' 2>/dev/null) + local data_type=$(echo "$action_config" | jq -r '.data_type // "snapshot"' 2>/dev/null) + + if [[ -f "$SCRIPT_DIR/data-export.sh" ]]; then + source "$SCRIPT_DIR/data-export.sh" + result=$(scheduled_export "$data_type" "$format") + fi + ;; + *) + result="Unknown action type: $action_type" + exit_code=1 + ;; + esac + + local end_time=$(date +%s) + local duration=$((end_time - start_time)) + + echo "{\"exit_code\": $exit_code, \"duration\": $duration, \"result\": \"$(echo "$result" | head -c 500 | tr '\n' ' ')\"}" + return $exit_code +} + +#─────────────────────────────────────────────────────────────────────────────── +# WORKFLOW EXECUTION +#─────────────────────────────────────────────────────────────────────────────── + +# Execute a workflow +workflow_execute() { + local workflow_name="$1" + local trigger_source="${2:-manual}" + + local workflow_file="${WORKFLOWS[$workflow_name]}" + [[ ! -f "$workflow_file" ]] && { + log_error "Workflow not found: $workflow_name" + return 1 + } + + local execution_id="exec_$(date +%s%N)" + local log_file="$WORKFLOW_LOGS/${workflow_name}_${execution_id}.log" + + WORKFLOW_STATUS[$workflow_name]="running" + log_info "Executing workflow: $workflow_name (ID: $execution_id)" + + { + echo "=== Workflow Execution ===" + echo "Name: $workflow_name" + echo "ID: $execution_id" + echo "Trigger: $trigger_source" + echo "Started: $(date)" + echo "==========================" + echo "" + } > "$log_file" + + # Check conditions + if command -v jq &>/dev/null; then + local conditions=$(jq -c '.conditions[]?' "$workflow_file" 2>/dev/null) + + while IFS= read -r condition; do + [[ -z "$condition" ]] && continue + + local cond_type=$(echo "$condition" | jq -r '.type') + local cond_config=$(echo "$condition" | jq -c '.config') + + if ! condition_evaluate "$cond_type" "$cond_config"; then + log_warn "Workflow condition not met: $cond_type" + echo "SKIPPED: Condition not met ($cond_type)" >> "$log_file" + WORKFLOW_STATUS[$workflow_name]="skipped" + return 0 + fi + done <<< "$conditions" + fi + + # Execute actions + local success=true + local action_count=0 + + if command -v jq &>/dev/null; then + local actions=$(jq -c '.actions[]?' "$workflow_file" 2>/dev/null) + + while IFS= read -r action; do + [[ -z "$action" ]] && continue + + ((action_count++)) + local action_type=$(echo "$action" | jq -r '.type') + local action_config=$(echo "$action" | jq -c '.config') + local action_id=$(echo "$action" | jq -r '.id') + + echo "--- Action $action_count: $action_type ($action_id) ---" >> "$log_file" + + local result + result=$(action_execute "$action_type" "$action_config" "$workflow_name") + local exit_code=$? + + echo "Result: $result" >> "$log_file" + echo "" >> "$log_file" + + if [[ $exit_code -ne 0 ]]; then + success=false + log_error "Action failed: $action_type (exit: $exit_code)" + + # Check if we should continue on error + local continue_on_error=$(jq -r '.settings.continue_on_error // false' "$workflow_file" 2>/dev/null) + [[ "$continue_on_error" != "true" ]] && break + fi + done <<< "$actions" + fi + + # Execute on_success or on_failure hooks + local hook_type="on_success" + [[ "$success" != "true" ]] && hook_type="on_failure" + + if command -v jq &>/dev/null; then + local hooks=$(jq -c ".${hook_type}[]?" "$workflow_file" 2>/dev/null) + + while IFS= read -r hook; do + [[ -z "$hook" ]] && continue + local hook_type=$(echo "$hook" | jq -r '.type') + local hook_config=$(echo "$hook" | jq -c '.config') + action_execute "$hook_type" "$hook_config" "$workflow_name" >> "$log_file" 2>&1 + done <<< "$hooks" + fi + + # Update status + local final_status="completed" + [[ "$success" != "true" ]] && final_status="failed" + + WORKFLOW_STATUS[$workflow_name]="$final_status" + WORKFLOW_LAST_RUN[$workflow_name]=$(date +%s) + + { + echo "" + echo "==========================" + echo "Completed: $(date)" + echo "Status: $final_status" + echo "Actions: $action_count" + } >> "$log_file" + + log_info "Workflow $workflow_name completed: $final_status" + return $([[ "$success" == "true" ]] && echo 0 || echo 1) +} + +#─────────────────────────────────────────────────────────────────────────────── +# WORKFLOW BUILDER UI +#─────────────────────────────────────────────────────────────────────────────── + +workflow_builder() { + clear_screen + cursor_hide + + local current_workflow="" + local mode="list" + + while true; do + cursor_to 1 1 + + printf "${BR_ORANGE}${BOLD}" + printf "╔══════════════════════════════════════════════════════════════════════════════╗\n" + printf "║ ⚡ BLACKROAD WORKFLOW ENGINE ║\n" + printf "╚══════════════════════════════════════════════════════════════════════════════╝\n" + printf "${RST}\n" + + case "$mode" in + list) + printf "${BOLD}Available Workflows:${RST}\n\n" + + local idx=1 + for name in "${!WORKFLOWS[@]}"; do + local file="${WORKFLOWS[$name]}" + local status="${WORKFLOW_STATUS[$name]:-idle}" + local last_run="${WORKFLOW_LAST_RUN[$name]:-never}" + + local status_color="$TEXT_MUTED" + case "$status" in + running) status_color="$BR_CYAN" ;; + completed) status_color="$BR_GREEN" ;; + failed) status_color="$BR_RED" ;; + esac + + [[ "$last_run" != "never" ]] && last_run=$(time_ago "$(($(date +%s) - last_run))") + + printf " ${BR_ORANGE}%d.${RST} ${BOLD}%-25s${RST} ${status_color}[%-10s]${RST} ${TEXT_MUTED}Last: %s${RST}\n" \ + "$idx" "$name" "$status" "$last_run" + ((idx++)) + done + + [[ $idx -eq 1 ]] && printf " ${TEXT_MUTED}No workflows defined.${RST}\n" + + printf "\n${TEXT_MUTED}─────────────────────────────────────────────────────────────────────────────${RST}\n" + printf " ${TEXT_SECONDARY}[n]ew [r]un <name> [e]dit <name> [d]elete <name> [l]ogs [q]uit${RST}\n" + ;; + esac + + printf "\n${BR_CYAN}> ${RST}" + cursor_show + read -r cmd args + cursor_hide + + case "$cmd" in + n|new) + printf "${BR_CYAN}Workflow name: ${RST}" + cursor_show + read -r wf_name + cursor_hide + + printf "${BR_CYAN}Description: ${RST}" + cursor_show + read -r wf_desc + cursor_hide + + workflow_create "$wf_name" "$wf_desc" + workflow_load_all + printf "${BR_GREEN}Workflow created!${RST}" + sleep 1 + ;; + r|run) + if [[ -n "$args" ]]; then + printf "${BR_CYAN}Running workflow: $args${RST}\n" + workflow_execute "$args" "manual" + fi + sleep 2 + ;; + d|delete) + if [[ -n "$args" ]] && [[ -f "${WORKFLOWS[$args]}" ]]; then + rm -f "${WORKFLOWS[$args]}" + unset WORKFLOWS[$args] + printf "${BR_GREEN}Workflow deleted.${RST}" + sleep 1 + fi + ;; + l|logs) + printf "\n${BOLD}Recent Logs:${RST}\n" + ls -lt "$WORKFLOW_LOGS"/*.log 2>/dev/null | head -10 + printf "\n${TEXT_MUTED}Press any key...${RST}" + read -rsn1 + ;; + q|quit) + break + ;; + esac + done + + cursor_show +} + +#─────────────────────────────────────────────────────────────────────────────── +# SAMPLE WORKFLOWS +#─────────────────────────────────────────────────────────────────────────────── + +create_sample_workflows() { + # High CPU Alert workflow + workflow_create "high-cpu-alert" "Alert when CPU exceeds 90%" + workflow_add_trigger "high-cpu-alert" "metric_threshold" '{"metric": "cpu", "operator": ">", "value": 90}' + workflow_add_action "high-cpu-alert" "notify" '{"level": "warning", "title": "High CPU Alert", "message": "CPU usage exceeded 90%"}' + workflow_add_action "high-cpu-alert" "log" '{"level": "warn", "message": "High CPU detected by workflow"}' + + # Daily backup workflow + workflow_create "daily-backup" "Daily system state backup" + workflow_add_condition "daily-backup" "time_window" '{"start_hour": 2, "end_hour": 4}' + workflow_add_action "daily-backup" "export" '{"format": "json", "data_type": "snapshot"}' + workflow_add_action "daily-backup" "notify" '{"level": "info", "title": "Backup Complete", "message": "Daily backup finished"}' + + # Health check workflow + workflow_create "health-check" "Periodic health verification" + workflow_add_action "health-check" "shell" '{"command": "echo Health check at $(date)"}' + workflow_add_action "health-check" "http" '{"url": "https://api.github.com", "method": "GET"}' + + log_info "Sample workflows created" +} + +#─────────────────────────────────────────────────────────────────────────────── +# MAIN +#─────────────────────────────────────────────────────────────────────────────── + +# Auto-load workflows +workflow_load_all + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + case "${1:-builder}" in + builder) workflow_builder ;; + run) workflow_execute "$2" "${3:-cli}" ;; + create) workflow_create "$2" "$3" ;; + list) + for name in "${!WORKFLOWS[@]}"; do + echo "$name: ${WORKFLOWS[$name]}" + done + ;; + samples) create_sample_workflows ;; + *) + printf "Usage: %s [builder|run|create|list|samples]\n" "$0" + printf " %s run <workflow_name>\n" "$0" + ;; + esac +fi