Posted on
Artificial Intelligence

Artificial Intelligence Reporting Dashboards

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Reporting Dashboards on Linux: From Bash to Insights

If you’re like most engineers, you already have dashboards—lots of them. But most dashboards only show what happened, not why. An AI-augmented reporting dashboard turns raw metrics into explanations: spotting anomalies, surfacing trends, and recommending actions automatically. And you don’t need GPUs or heavy tooling to get started—you can build a lightweight, explainable AI dashboard on any Linux box with Bash, Python, and a few open-source libraries.

This guide shows you how to stand up a minimal, fast, and reliable AI-enabled reporting dashboard using:

  • Bash for data collection

  • pandas and scikit-learn for analysis and anomaly detection

  • Flask and Chart.js for a simple web UI

  • systemd timers for hands-off automation

We’ll cover end-to-end setup (including apt, dnf, and zypper installs), and give you actionable steps you can adapt to your own metrics.


Why AI Dashboards (on Linux) Are Worth Your Time

  • They summarize, not just visualize. Classic dashboards show charts. AI dashboards add context—“CPU load jumped 35% vs. baseline,” “Memory usage is trending up,” “Here are likely causes.”

  • They work with what you already have. A few CSVs or logs are enough. You don’t need a data warehouse to get value from anomaly detection and simple trend models.

  • They’re simple to operate. With Bash + Python + systemd, you can run this on a tiny VM or even a Raspberry Pi—no Kubernetes cluster required.

  • They’re explainable. Using classical ML (e.g., IsolationForest) and rolling statistics, you can keep the logic transparent and tunable.


What You’ll Build

We’ll collect local system metrics (CPU load and memory usage) into a CSV, analyze them for anomalies and trends, and serve a small web dashboard that updates continuously.

Stack:

  • Data collection: Bash script (reads /proc)

  • Analysis: pandas + scikit-learn (IsolationForest)

  • Web UI: Flask (Python) + Chart.js (no Node required)

  • Automation: systemd user timer


1) Install prerequisites

Install system packages.

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y python3 python3-venv python3-pip git nginx
  • Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y python3 python3-pip git nginx

(Note: Python’s venv is included in python3 on most Fedora-based systems. If you need virtualenv specifically: sudo dnf install -y python3-virtualenv)

  • openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y python3 python3-pip git nginx

(Note: Python’s venv is typically included in python3 on openSUSE. If you need virtualenv: sudo zypper install -y python3-virtualenv)


2) Scaffold the project and Python environment

mkdir -p ~/ai-dashboard && cd ~/ai-dashboard

python3 -m venv .venv
source .venv/bin/activate

pip install --upgrade pip
pip install flask pandas scikit-learn

Directory outline:

ai-dashboard/
  .venv/
  data/
  metrics_collect.sh
  analyze_and_serve.py

3) Collect metrics with Bash (every minute)

Create a simple collector that writes a CSV with timestamp, 1-minute load average, and memory usage percent.

Create data dir and script:

mkdir -p data
cat > metrics_collect.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

DATA_DIR="${DATA_DIR:-$(pwd)/data}"
mkdir -p "$DATA_DIR"
CSV="$DATA_DIR/metrics.csv"

if [ ! -f "$CSV" ]; then
  echo "timestamp,load1,mem_used_pct" > "$CSV"
fi

ts="$(date -Iseconds)"
load1="$(awk '{print $1}' /proc/loadavg)"
mem_used_pct="$(awk '
  /MemTotal/ {t=$2}
  /MemAvailable/ {a=$2}
  END {printf "%.2f", (1 - a/t) * 100}
' /proc/meminfo)"

echo "$ts,$load1,$mem_used_pct" >> "$CSV"
EOF
chmod +x metrics_collect.sh

Run it once to generate initial data:

./metrics_collect.sh

Automate with a systemd user timer (recommended; no root required):

Service unit:

mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/ai-metrics-collector.service << 'EOF'
[Unit]
Description=Collect system metrics to CSV

[Service]
Type=oneshot
WorkingDirectory=%h/ai-dashboard
ExecStart=%h/ai-dashboard/metrics_collect.sh
EOF

Timer unit (run every minute):

cat > ~/.config/systemd/user/ai-metrics-collector.timer << 'EOF'
[Unit]
Description=Run metrics collector every minute

[Timer]
OnCalendar=*:0/1
Persistent=true
Unit=ai-metrics-collector.service

[Install]
WantedBy=timers.target
EOF

Enable and start the timer:

systemctl --user daemon-reload
systemctl --user enable --now ai-metrics-collector.timer

# Check status and recent runs
systemctl --user status ai-metrics-collector.timer
journalctl --user -u ai-metrics-collector.service -n 20 --no-pager

Tip: To use user services across SSH sessions, ensure lingering is enabled for your user:

sudo loginctl enable-linger "$USER"

4) Analyze and serve a dashboard (Flask + Chart.js)

Create a single Python app that:

  • Loads CSV data

  • Computes rolling trends and detects anomalies with IsolationForest

  • Serves a small web UI

Create analyze_and_serve.py:

cat > analyze_and_serve.py << 'EOF'
#!/usr/bin/env python3
import os
from pathlib import Path
import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
from flask import Flask, jsonify, render_template_string

DATA_CSV = Path("data/metrics.csv")

def load_df():
    if not DATA_CSV.exists():
        return pd.DataFrame(columns=["timestamp","load1","mem_used_pct"])
    df = pd.read_csv(DATA_CSV, parse_dates=["timestamp"])
    if not df.empty:
        df = df.sort_values("timestamp")
    return df

def analyze(df: pd.DataFrame):
    if df.empty:
        return df, {
            "samples": 0,
            "trend": {"load1_pct_change": 0.0, "mem_used_pct_change": 0.0},
            "anomalies_recent": [],
            "recommendations": ["No data yet. Waiting for the first samples..."]
        }

    # Prepare features
    X = df[["load1", "mem_used_pct"]].copy()
    X = X.replace([np.inf, -np.inf], np.nan).fillna(method="ffill").fillna(0.0)

    # Anomaly detection (robust, simple)
    if len(X) >= 30:
        iso = IsolationForest(contamination=0.05, random_state=42)
        df["anomaly"] = (iso.fit_predict(X) == -1)
    else:
        df["anomaly"] = False

    # Rolling averages for light smoothing
    df["load1_rolling"] = df["load1"].rolling(window=15, min_periods=3).mean()
    df["mem_rolling"]  = df["mem_used_pct"].rolling(window=15, min_periods=3).mean()

    # Simple trend: compare recent window to prior baseline
    tail = df.tail(15)
    prev = df.tail(90).head(60)  # baseline window
    def pct_change(a, b):
        if len(a) == 0 or len(b) == 0 or b.mean() == 0:
            return 0.0
        return float(((a.mean() - b.mean()) / (b.mean() + 1e-9)) * 100.0)

    trend_load = pct_change(tail["load1"], prev["load1"])
    trend_mem  = pct_change(tail["mem_used_pct"], prev["mem_used_pct"])

    top_anoms = df[df["anomaly"]].tail(5)[["timestamp","load1","mem_used_pct"]]
    insights = {
        "samples": int(len(df)),
        "trend": {
            "load1_pct_change": round(trend_load, 2),
            "mem_used_pct_change": round(trend_mem, 2),
        },
        "anomalies_recent": [
            {
                "timestamp": pd.to_datetime(ts).isoformat(),
                "load1": float(l),
                "mem_used_pct": float(m)
            }
            for ts, l, m in zip(top_anoms["timestamp"], top_anoms["load1"], top_anoms["mem_used_pct"])
        ],
        "recommendations": []
    }

    # Simple, explainable recommendations
    recent_mem = float(df["mem_used_pct"].tail(1).iloc[0])
    recent_anoms = int(df["anomaly"].tail(10).sum())
    if trend_load > 25 or recent_anoms >= 2:
        insights["recommendations"].append(
            "Investigate recent CPU load spikes; check top processes (top/htop) and recent deploys."
        )
    if recent_mem > 85:
        insights["recommendations"].append(
            "High memory usage detected (>85%). Inspect memory-heavy services, leaks, or increase memory."
        )
    if not insights["recommendations"]:
        insights["recommendations"].append("System looks healthy in the last window.")

    return df, insights

app = Flask(__name__)

INDEX_HTML = """<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AI Reporting Dashboard</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: system-ui, sans-serif; margin: 20px; }
    .grid { display: grid; grid-template-columns: 1fr; gap: 18px; }
    @media (min-width: 900px) { .grid { grid-template-columns: 1fr 1fr; } }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 14px; }
    .good { color: #177245; } .warn { color: #9B870C; } .bad { color: #B22222; }
    code { background: #f8f8f8; padding: 2px 4px; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h1>AI Reporting Dashboard</h1>
<p>Lightweight anomaly detection and trend insights, powered by pandas + scikit-learn.</p>

<div class="grid">
  <div class="card">
    <h3>CPU Load (1m)</h3>
    <canvas id="loadChart" height="160"></canvas>
  </div>
  <div class="card">
    <h3>Memory Usage (%)</h3>
    <canvas id="memChart" height="160"></canvas>
  </div>
  <div class="card">
    <h3>Insights</h3>
    <ul id="insights"></ul>
  </div>
  <div class="card">
    <h3>Recent Anomalies</h3>
    <ul id="anoms"></ul>
  </div>
</div>

<script>
async function fetchJSON(url){ const r = await fetch(url); return await r.json(); }

let loadChart, memChart;

function mkChart(ctx, label, color){
  return new Chart(ctx, {
    type: 'line',
    data: { labels: [], datasets: [
      { label: label, data: [], borderColor: color, tension: 0.2, pointRadius: 0 }
    ]},
    options: {
      responsive: true,
      scales: { x: { display: false }, y: { beginAtZero: false } },
      plugins: { legend: { display: true } }
    }
  });
}

async function refresh(){
  const metrics = await fetchJSON('/api/metrics');
  const ins = await fetchJSON('/api/insights');

  if(!loadChart){
    loadChart = mkChart(document.getElementById('loadChart'), 'load1', '#1f77b4');
    memChart  = mkChart(document.getElementById('memChart'),  'mem %', '#ff7f0e');
  }

  loadChart.data.labels = metrics.timestamps;
  loadChart.data.datasets[0].data = metrics.load1;
  loadChart.update('none');

  memChart.data.labels = metrics.timestamps;
  memChart.data.datasets[0].data = metrics.mem_used_pct;
  memChart.update('none');

  const insightEl = document.getElementById('insights');
  insightEl.innerHTML = '';
  const tLoad = ins.trend.load1_pct_change;
  const tMem  = ins.trend.mem_used_pct_change;
  insightEl.innerHTML += `<li>Samples: <code>${ins.samples}</code></li>`;
  insightEl.innerHTML += `<li>Load trend: <b>${tLoad}%</b></li>`;
  insightEl.innerHTML += `<li>Memory trend: <b>${tMem}%</b></li>`;
  ins.recommendations.forEach(r => {
    const li = document.createElement('li');
    li.textContent = r;
    insightEl.appendChild(li);
  });

  const anoms = document.getElementById('anoms');
  anoms.innerHTML = '';
  ins.anomalies_recent.forEach(a => {
    const li = document.createElement('li');
    li.textContent = `${a.timestamp} — load1=${a.load1.toFixed(2)}, mem=${a.mem_used_pct.toFixed(1)}%`;
    anoms.appendChild(li);
  });
}

setInterval(refresh, 30000);
refresh();
</script>
</body>
</html>
"""

@app.get("/")
def index():
    return render_template_string(INDEX_HTML)

@app.get("/api/metrics")
def api_metrics():
    df = load_df()
    df, _ = analyze(df)
    dfl = df.tail(1000) if len(df) > 1000 else df
    out = {
        "timestamps": dfl["timestamp"].dt.strftime("%Y-%m-%d %H:%M:%S").tolist(),
        "load1": [float(x) for x in dfl["load1"].tolist()],
        "mem_used_pct": [float(x) for x in dfl["mem_used_pct"].tolist()],
        "anomaly": [int(x) for x in dfl["anomaly"].astype(int).tolist()]
    }
    return jsonify(out)

@app.get("/api/insights")
def api_insights():
    df = load_df()
    _, insights = analyze(df)
    return jsonify(insights)

if __name__ == "__main__":
    port = int(os.environ.get("PORT", "8000"))
    app.run(host="0.0.0.0", port=port)
EOF
chmod +x analyze_and_serve.py

Run the server:

source .venv/bin/activate
python analyze_and_serve.py
# Visit http://127.0.0.1:8000

Let it run while the systemd timer appends new rows each minute; your dashboard will update on refresh and every 30 seconds.


5) Optional: Serve behind Nginx

If you don’t already have Nginx:

  • Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y nginx
  • Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y nginx
sudo systemctl enable --now nginx
  • openSUSE (zypper):
sudo zypper install -y nginx
sudo systemctl enable --now nginx

Add a simple reverse proxy (e.g., /etc/nginx/conf.d/ai-dashboard.conf):

server {
    listen 80;
    server_name dashboard.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Actionable Tips and Real-World Extensions

1) Add your business metrics

  • Append rows from your job queues, API counters, or database stats into the same CSV (or another one).

  • You can add more columns (e.g., requests_per_min, error_rate) and update analyze() to include them in anomaly detection.

2) Parse Nginx access logs for error trends

# Example: count 5xx responses in the last minute
awk '($9 ~ /^5/) {print $4}' /var/log/nginx/access.log | wc -l

Better: pre-aggregate by minute with log-format that includes $status and $request_time, then write into CSV columns like error_5xx and p95_latency_ms.

3) Make anomalies actionable

  • Page on repeated anomalies: trigger a webhook when df["anomaly"].tail(10).sum() >= N.

  • Tie insights to runbooks: return URLs to diagnostics (Grafana, Loki queries, CI/CD changes).

4) Keep it explainable and fast

  • Tune IsolationForest’s contamination (e.g., 0.01–0.1) for your data.

  • Prefer rolling statistics and simple baselines before going deep into heavy models.

5) Automate everything

  • Use systemd timers for each ingest (service + timer per source).

  • Add a systemd user service for the Flask app or run it with a process manager (e.g., gunicorn + systemd).


Troubleshooting

  • scikit-learn install issues: Ensure you’re on a supported architecture; upgrade pip; try a clean venv. Wheels are available for mainstream distros; compilation is rarely needed.

  • Empty dashboard: Wait a minute for the first few samples; check journalctl for the collector timer.

  • High CPU load in visuals: Limit points (we capped at 1000) or downsample older data.


Conclusion and Next Steps (CTA)

You just built an AI-enabled reporting dashboard that runs anywhere Linux does—no GPUs, no vendor lock-in. From here:

  • Add your own metrics: business KPIs, request errors, queue depths.

  • Expand the analyzer: include error_rate and p95_latency, add domain rules, and tune anomaly sensitivity.

  • Productionize it: run behind Nginx with TLS, add authentication, and ship metrics and insights to long-term storage.

If this helped you ship more signal and less noise, take 30 minutes to wire in one business metric today—you’ll get immediate value from AI-assisted insights without changing your stack.