diff --git a/adguard-leaderboard/README.md b/adguard-leaderboard/README.md new file mode 100644 index 0000000..509bd59 --- /dev/null +++ b/adguard-leaderboard/README.md @@ -0,0 +1,87 @@ +# AdGuard Home Leaderboard Dashboard + +A Flask-based dashboard that pulls DNS query stats from AdGuard Home and displays clients as anime character tier cards. + +## Project Structure + +``` +adguard-dashboard/ +├── dashboard.py # Flask app and AdGuard API integration +├── dashboard.wsgi # Apache mod_wsgi entry point +├── templates/ +│ └── dashboard.html # Frontend tier card UI +├── static/ +│ └── characters/ # Anime character images (tier1_1.png, etc.) +├── .env # Credentials (not committed to repo) +└── README.md +``` + +## Tiers + +| Tier | Label | Query Range | Character(s) | +|------|----------------|--------------|--------------------| +| 1 | Innocent | 0 - 500 | Totoro | +| 2 | Rookie | 501 - 2000 | Zenitsu | +| 3 | Protagonist | 2001 - 5000 | Spike Spiegel | +| 4 | Hardened Fighter | 5001 - 10000 | Vegeta, Mikasa, Endeavor, Zoro, Inuyasha | +| 5 | Final Boss | 10001+ | Eren Yeager (Founding Titan) | + +Each tier randomly selects a character image on page load. Images are stored as `tier{N}_{num}.png` in `static/characters/`. + +## Setup + +### Prerequisites + +- Python 3 +- Apache with mod_wsgi +- AdGuard Home instance + +### Installation + +```bash +sudo mkdir -p /opt/adguard-dashboard/templates +sudo mkdir -p /opt/adguard-dashboard/static/characters +sudo python3 -m venv /opt/adguard-dashboard/venv +sudo /opt/adguard-dashboard/venv/bin/pip install flask requests python-dotenv +``` + +Copy files: +```bash +sudo cp dashboard.py /opt/adguard-dashboard/ +sudo cp dashboard.wsgi /opt/adguard-dashboard/ +sudo cp templates/dashboard.html /opt/adguard-dashboard/templates/ +``` + +### Environment Variables + +Create `/opt/adguard-dashboard/.env`: +``` +ADGUARD_HOST=http://192.168.1.235:30004 +ADGUARD_USER=your_username +ADGUARD_PASS=your_password +``` + +Lock down permissions: +```bash +sudo chown root:www-data /opt/adguard-dashboard/.env +sudo chmod 640 /opt/adguard-dashboard/.env +``` + +### Apache Configuration + +Add a config in `/etc/apache2/sites-available/` pointing to `dashboard.wsgi` via mod_wsgi. + +To reload after changes: +```bash +sudo touch /opt/adguard-dashboard/dashboard.wsgi +``` + +## Adding Characters + +Drop images into `static/characters/` named `tier{N}_{num}.png` (e.g. `tier3_2.png`), then update the `TIER_CHARS` object in `dashboard.html` to include the new filename in the appropriate tier array. +```javascript +const TIER_CHARS = { + 1: ['tier1_1.png', 'tier1_2.png'], + ... +}; +``` \ No newline at end of file diff --git a/adguard-leaderboard/dashboard.py b/adguard-leaderboard/dashboard.py new file mode 100644 index 0000000..07e7ec8 --- /dev/null +++ b/adguard-leaderboard/dashboard.py @@ -0,0 +1,93 @@ +from flask import Flask, render_template, jsonify +import requests +import os +from requests.auth import HTTPBasicAuth +from dotenv import load_dotenv + +load_dotenv('/opt/adguard-dashboard/.env') + +app = Flask(__name__) + +ADGUARD_HOST = os.environ.get("ADGUARD_HOST") +ADGUARD_USER = os.environ.get("ADGUARD_USER") +ADGUARD_PASS = os.environ.get("ADGUARD_PASS") +EXCLUDE_CLIENTS = ["192.168.1.235"] + + +def get_tier(count): + if count <= 500: + return {"tier": 1, "label": "Innocent", "desc": "Blissfully unaware", "color": "#4ade80"} + elif count <= 2000: + return {"tier": 2, "label": "Rookie", "desc": "Starting to notice things", "color": "#facc15"} + elif count <= 5000: + return {"tier": 3, "label": "Protagonist", "desc": "Things are getting real", "color": "#fb923c"} + elif count <= 10000: + return {"tier": 4, "label": "Hardened Fighter", "desc": "Seen some things", "color": "#f87171"} + else: + return {"tier": 5, "label": "Final Boss", "desc": "Pure chaos energy", "color": "#c084fc"} + + +def get_client_names(): + try: + resp = requests.get( + f"{ADGUARD_HOST}/control/clients", + auth=HTTPBasicAuth(ADGUARD_USER, ADGUARD_PASS), + timeout=5 + ) + resp.raise_for_status() + data = resp.json() + lookup = {} + for client in data.get("clients", []): + for ip in client.get("ids", []): + lookup[ip] = client["name"] + return lookup + except Exception: + return {} + + +@app.route("/") +def index(): + return render_template("dashboard.html") + + +@app.route("/api/clients") +def clients(): + try: + names = get_client_names() + + resp = requests.get( + f"{ADGUARD_HOST}/control/stats", + auth=HTTPBasicAuth(ADGUARD_USER, ADGUARD_PASS), + timeout=5 + ) + resp.raise_for_status() + data = resp.json() + + top_clients = data.get("top_clients", []) + + result = [] + for entry in top_clients: + for ip, count in entry.items(): + if ip in EXCLUDE_CLIENTS: + continue + tier_info = get_tier(count) + result.append({ + "name": names.get(ip, ip), + "ip": ip, + "blocks": count, + **tier_info + }) + + result.sort(key=lambda x: x["blocks"], reverse=True) + return jsonify({"clients": result, "error": None}) + + except requests.exceptions.ConnectionError: + return jsonify({"clients": [], "error": "Could not connect to AdGuard Home"}) + except requests.exceptions.Timeout: + return jsonify({"clients": [], "error": "AdGuard Home timed out"}) + except Exception as e: + return jsonify({"clients": [], "error": str(e)}) + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5001) \ No newline at end of file diff --git a/adguard-leaderboard/dashboard.wsgi b/adguard-leaderboard/dashboard.wsgi new file mode 100644 index 0000000..7a662ea --- /dev/null +++ b/adguard-leaderboard/dashboard.wsgi @@ -0,0 +1,11 @@ +import sys +import os + +sys.path.insert(0, '/opt/adguard-dashboard') + +# Activate virtualenv +activate_this = '/opt/adguard-dashboard/venv/bin/activate_this.py' +with open(activate_this) as f: + exec(f.read(), {'__file__': activate_this}) + +from dashboard import app as application diff --git a/adguard-leaderboard/templates/dashboard.html b/adguard-leaderboard/templates/dashboard.html new file mode 100644 index 0000000..2ef4b98 --- /dev/null +++ b/adguard-leaderboard/templates/dashboard.html @@ -0,0 +1,224 @@ + + +
+ + +Who's been out there in these internet streets?
+ +