How to Build a Multi-Account Ad Management Dashboard
Manage multiple ad accounts across platforms from one unified interface.
Jay Banlasan
The AI Systems Guy
A multi-account ad management dashboard tool shows every client's performance on one screen. I manage 10+ ad accounts and this saved me from the daily ritual of logging into each one separately. One page, all accounts, real-time status.
The dashboard pulls from the same unified database your daily scripts populate. No new data sources needed.
What You Need Before Starting
- A unified SQLite database with data from all accounts
- Python 3.8+ with Flask installed
- Daily data pull scripts already running
- Basic HTML/CSS knowledge
Step 1: Build the Account Summary Query
import sqlite3
from datetime import datetime, timedelta
def get_all_accounts_summary(db_path):
conn = sqlite3.connect(db_path)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
accounts = conn.execute("""
SELECT account_name,
SUM(spend) as spend,
SUM(clicks) as clicks,
SUM(leads) as leads,
SUM(impressions) as impressions,
CASE WHEN SUM(leads)>0 THEN SUM(spend)/SUM(leads) ELSE 0 END as cpl,
CASE WHEN SUM(impressions)>0 THEN SUM(clicks)*100.0/SUM(impressions) ELSE 0 END as ctr
FROM ad_daily
WHERE date = ?
GROUP BY account_name
ORDER BY spend DESC
""", (yesterday,)).fetchall()
return [{"account": r[0], "spend": r[1], "clicks": r[2], "leads": r[3],
"impressions": r[4], "cpl": round(r[5], 2), "ctr": round(r[6], 2)} for r in accounts]
def get_account_detail(db_path, account_name, days=7):
conn = sqlite3.connect(db_path)
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
daily = conn.execute("""
SELECT date, SUM(spend), SUM(leads),
CASE WHEN SUM(leads)>0 THEN SUM(spend)/SUM(leads) ELSE 0 END
FROM ad_daily WHERE account_name = ? AND date >= ?
GROUP BY date ORDER BY date
""", (account_name, start)).fetchall()
conn.close()
return [{"date": r[0], "spend": r[1], "leads": r[2], "cpl": round(r[3], 2)} for r in daily]
Step 2: Create the Dashboard Server
from flask import Flask, render_template_string, jsonify
app = Flask(__name__)
DB_PATH = "meta_ads.db"
DASHBOARD_TEMPLATE = """
<!DOCTYPE html>
<html>
<head><title>Ad Dashboard</title>
<style>
body { font-family: -apple-system, sans-serif; background: #0a0a0a; color: #e5e7eb; padding: 2rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; }
.card { background: #111827; border: 1px solid #1f2937; border-radius: 8px; padding: 1.5rem; }
.card h3 { margin: 0 0 1rem; color: #f3f4f6; }
.metric { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #1f2937; }
.metric-value { font-weight: 600; color: #60a5fa; }
.status-good { color: #34d399; }
.status-bad { color: #f87171; }
h1 { color: #f9fafb; margin-bottom: 2rem; }
</style>
</head>
<body>
<h1>Ad Accounts Overview</h1>
<div class="grid">
{% for account in accounts %}
<div class="card">
<h3>{{ account.account }}</h3>
<div class="metric"><span>Spend</span><span class="metric-value">${{ "%.2f"|format(account.spend) }}</span></div>
<div class="metric"><span>Leads</span><span class="metric-value">{{ account.leads }}</span></div>
<div class="metric"><span>CPL</span><span class="metric-value {{ 'status-good' if account.cpl < 25 else 'status-bad' }}">${{ "%.2f"|format(account.cpl) }}</span></div>
<div class="metric"><span>CTR</span><span class="metric-value">{{ "%.2f"|format(account.ctr) }}%</span></div>
</div>
{% endfor %}
</div>
</body>
</html>
"""
@app.route("/")
def dashboard():
accounts = get_all_accounts_summary(DB_PATH)
return render_template_string(DASHBOARD_TEMPLATE, accounts=accounts)
@app.route("/api/account/<account_name>")
def account_detail(account_name):
detail = get_account_detail(DB_PATH, account_name)
return jsonify(detail)
Step 3: Add Health Status Indicators
def get_account_health(db_path, account_name):
conn = sqlite3.connect(db_path)
yesterday_spend = conn.execute("""
SELECT COALESCE(SUM(spend), 0) FROM ad_daily
WHERE account_name = ? AND date = DATE('now', '-1 day')
""", (account_name,)).fetchone()[0]
avg_spend = conn.execute("""
SELECT COALESCE(AVG(daily_spend), 0) FROM (
SELECT SUM(spend) as daily_spend FROM ad_daily
WHERE account_name = ? AND date >= DATE('now', '-7 days')
GROUP BY date
)
""", (account_name,)).fetchone()[0]
conn.close()
if yesterday_spend == 0:
return "offline"
elif avg_spend > 0 and yesterday_spend < avg_spend * 0.5:
return "warning"
elif avg_spend > 0 and yesterday_spend > avg_spend * 1.5:
return "spike"
return "healthy"
Step 4: Add Quick Actions
@app.route("/api/pause/<campaign_id>", methods=["POST"])
def pause_campaign(campaign_id):
import requests
token = os.getenv("META_ACCESS_TOKEN")
resp = requests.post(f"https://graph.facebook.com/v19.0/{campaign_id}",
params={"access_token": token, "status": "PAUSED"})
return jsonify(resp.json())
@app.route("/api/activate/<campaign_id>", methods=["POST"])
def activate_campaign(campaign_id):
import requests
token = os.getenv("META_ACCESS_TOKEN")
resp = requests.post(f"https://graph.facebook.com/v19.0/{campaign_id}",
params={"access_token": token, "status": "ACTIVE"})
return jsonify(resp.json())
Step 5: Deploy and Access
# Run on your VPS
pip install flask gunicorn
gunicorn -b 0.0.0.0:8080 dashboard:app --daemon
Access via http://your-vps-ip:8080. For security, add basic auth or restrict to your IP.
One dashboard for everything. Click into any account for details. Pause or activate campaigns without opening Ads Manager.
What to Build Next
Add historical trend charts for each account. Then add a daily digest that screenshots the dashboard and sends it to Slack every morning.
Related Reading
- AI-Powered Reporting That Actually Gets Read - reporting that people use
- The Centralized Brain Concept - centralizing operational intelligence
- The One Person Company Is Here - managing at scale with one person
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment