fix(affiliate): sidebar active state, subnav order, dev seed data

- base_admin.html: add 'affiliate_dashboard' to _section_map so Dashboard
  page stays under the Affiliate section (was falling through to 'overview')
- base_admin.html: sidebar Affiliate link now points to dashboard (first tab)
- base_admin.html: subnav order Dashboard | Products (was Products | Dashboard)
- seed_dev_data.py: add 10 affiliate products (4 rackets, 2 shoes, 1 ball,
  1 grip, 1 bag) + 236 click events spread over 30 days for dashboard charts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-28 21:32:20 +01:00
parent 5c22ea9780
commit 0984657e72
2 changed files with 247 additions and 3 deletions

View File

@@ -99,7 +99,7 @@
'suppliers': 'suppliers', 'suppliers': 'suppliers',
'articles': 'content', 'scenarios': 'content', 'templates': 'content', 'pseo': 'content', 'articles': 'content', 'scenarios': 'content', 'templates': 'content', 'pseo': 'content',
'emails': 'email', 'inbox': 'email', 'compose': 'email', 'gallery': 'email', 'audiences': 'email', 'outreach': 'email', 'emails': 'email', 'inbox': 'email', 'compose': 'email', 'gallery': 'email', 'audiences': 'email', 'outreach': 'email',
'affiliate': 'affiliate', 'affiliate': 'affiliate', 'affiliate_dashboard': 'affiliate',
'billing': 'billing', 'billing': 'billing',
'seo': 'analytics', 'seo': 'analytics',
'pipeline': 'pipeline', 'pipeline': 'pipeline',
@@ -150,7 +150,7 @@
Billing Billing
</a> </a>
<a href="{{ url_for('admin.affiliate_products') }}" class="{% if active_section == 'affiliate' %}active{% endif %}"> <a href="{{ url_for('admin.affiliate_dashboard') }}" class="{% if active_section == 'affiliate' %}active{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349M3.75 21V9.349m0 0a3.001 3.001 0 0 0 3.75-.615A2.993 2.993 0 0 0 9.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 0 0 2.25 1.016 2.993 2.993 0 0 0 2.25-1.016 3.001 3.001 0 0 0 3.75.614m-16.5 0a3.004 3.004 0 0 1-.621-4.72l1.189-1.19A1.5 1.5 0 0 1 5.378 3h13.243a1.5 1.5 0 0 1 1.06.44l1.19 1.189a3 3 0 0 1-.621 4.72M6.75 18h3.75a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75H6.75a.75.75 0 0 0-.75.75v3.75c0 .414.336.75.75.75Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349M3.75 21V9.349m0 0a3.001 3.001 0 0 0 3.75-.615A2.993 2.993 0 0 0 9.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 0 0 2.25 1.016 2.993 2.993 0 0 0 2.25-1.016 3.001 3.001 0 0 0 3.75.614m-16.5 0a3.004 3.004 0 0 1-.621-4.72l1.189-1.19A1.5 1.5 0 0 1 5.378 3h13.243a1.5 1.5 0 0 1 1.06.44l1.19 1.189a3 3 0 0 1-.621 4.72M6.75 18h3.75a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75H6.75a.75.75 0 0 0-.75.75v3.75c0 .414.336.75.75.75Z"/></svg>
Affiliate Affiliate
</a> </a>
@@ -204,8 +204,8 @@
</nav> </nav>
{% elif active_section == 'affiliate' %} {% elif active_section == 'affiliate' %}
<nav class="admin-subnav"> <nav class="admin-subnav">
<a href="{{ url_for('admin.affiliate_products') }}" class="{% if admin_page == 'affiliate' %}active{% endif %}">Products</a>
<a href="{{ url_for('admin.affiliate_dashboard') }}" class="{% if admin_page == 'affiliate_dashboard' %}active{% endif %}">Dashboard</a> <a href="{{ url_for('admin.affiliate_dashboard') }}" class="{% if admin_page == 'affiliate_dashboard' %}active{% endif %}">Dashboard</a>
<a href="{{ url_for('admin.affiliate_products') }}" class="{% if admin_page == 'affiliate' %}active{% endif %}">Products</a>
</nav> </nav>
{% elif active_section == 'system' %} {% elif active_section == 'system' %}
<nav class="admin-subnav"> <nav class="admin-subnav">

View File

@@ -284,6 +284,184 @@ LEADS = [
] ]
AFFILIATE_PRODUCTS = [
# Rackets
{
"slug": "bullpadel-vertex-04-amazon",
"name": "Bullpadel Vertex 04",
"brand": "Bullpadel",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST01?tag=padelnomics-21",
"price_cents": 17999,
"rating": 4.7,
"pros": '["Carbon-Rahmen für maximale Power", "Diamant-Form für aggressive Spieler", "Sehr gute Balance"]',
"cons": '["Nur für fortgeschrittene Spieler", "Höherer Preis"]',
"description": "Der Vertex 04 ist der Flaggschiff-Schläger von Bullpadel für Power-Spieler.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "head-delta-pro-amazon",
"name": "HEAD Delta Pro",
"brand": "HEAD",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST02?tag=padelnomics-21",
"price_cents": 14999,
"rating": 4.5,
"pros": '["Sehr kontrollorientiert", "Ideal für Defensivspieler", "Leicht"]',
"cons": '["Weniger Power als Diamant-Formen"]',
"description": "Runde Form mit perfekter Kontrolle — ideal für Einsteiger und Defensivspieler.",
"status": "active",
"language": "de",
"sort_order": 2,
},
{
"slug": "adidas-metalbone-30-amazon",
"name": "Adidas Metalbone 3.0",
"brand": "Adidas",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST03?tag=padelnomics-21",
"price_cents": 18999,
"rating": 4.8,
"pros": '["Brutale Power", "Hochwertige Verarbeitung", "Sehr beliebt auf Pro-Tour"]',
"cons": '["Teuer", "Gewöhnungsbedürftig"]',
"description": "Das Flaggschiff von Adidas Padel — getragen von den besten Profis der Welt.",
"status": "active",
"language": "de",
"sort_order": 3,
},
{
"slug": "wilson-bela-pro-v2-amazon",
"name": "Wilson Bela Pro v2",
"brand": "Wilson",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST04?tag=padelnomics-21",
"price_cents": 16999,
"rating": 4.6,
"pros": '["Bekannter Signature-Schläger", "Gute Mischung aus Power und Kontrolle"]',
"cons": '["Fortgeschrittene bevorzugt"]',
"description": "Der Schläger von Fernando Belasteguín — einer der meistgekauften Schläger weltweit.",
"status": "active",
"language": "de",
"sort_order": 4,
},
# Beginner racket — draft (tests that draft products are excluded from public)
{
"slug": "dunlop-aero-star-amazon",
"name": "Dunlop Aero Star",
"brand": "Dunlop",
"category": "racket",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST05?tag=padelnomics-21",
"price_cents": 8999,
"rating": 4.2,
"pros": '["Günstig", "Für Einsteiger ideal"]',
"cons": '["Wenig Power für Fortgeschrittene"]',
"description": "Solider Einsteigerschläger für unter 90 Euro.",
"status": "draft",
"language": "de",
"sort_order": 5,
},
# Shoes
{
"slug": "adidas-adipower-ctrl-amazon",
"name": "Adidas Adipower Ctrl",
"brand": "Adidas",
"category": "shoe",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST10?tag=padelnomics-21",
"price_cents": 9999,
"rating": 4.4,
"pros": '["Hervorragender Halt auf Sand", "Leicht und atmungsaktiv"]',
"cons": '["Größenfehler möglich — eine Größe größer bestellen"]',
"description": "Professioneller Padelschuh mit optimierter Sohle für Sand- und Kunstrasencourts.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "babolat-jet-premura-amazon",
"name": "Babolat Jet Premura",
"brand": "Babolat",
"category": "shoe",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST11?tag=padelnomics-21",
"price_cents": 11999,
"rating": 4.6,
"pros": '["Sehr leicht", "Gute Dämpfung", "Stylisches Design"]',
"cons": '["Teurer als Mitbewerber"]',
"description": "Ultraleichter Padelschuh von Babolat — ideal für schnelle Spieler.",
"status": "active",
"language": "de",
"sort_order": 2,
},
# Balls
{
"slug": "head-padel-pro-balls-amazon",
"name": "HEAD Padel Pro Bälle (3er-Dose)",
"brand": "HEAD",
"category": "ball",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST20?tag=padelnomics-21",
"price_cents": 799,
"rating": 4.5,
"pros": '["Offizieller Turnierball", "Guter Druckerhalt", "Günstig"]',
"cons": '["Bei intensivem Spiel nach 45 Sessions platter"]',
"description": "Offizieller Turnierball von HEAD — der am häufigsten gespielte Padelball in Europa.",
"status": "active",
"language": "de",
"sort_order": 1,
},
# Grips/Accessories
{
"slug": "bullpadel-overgrip-3er-amazon",
"name": "Bullpadel Overgrip (3er-Pack)",
"brand": "Bullpadel",
"category": "grip",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST30?tag=padelnomics-21",
"price_cents": 499,
"rating": 4.3,
"pros": '["Günstig", "Guter Halt auch bei Schweiß", "Einfach zu wechseln"]',
"cons": '["Hält weniger lang als Originalgriff"]',
"description": "Günstiges Overgrip-Set — jeder Padelspieler sollte regelmäßig wechseln.",
"status": "active",
"language": "de",
"sort_order": 1,
},
{
"slug": "nox-padel-bag-amazon",
"name": "NOX ML10 Schläger-Tasche",
"brand": "NOX",
"category": "accessory",
"retailer": "Amazon",
"affiliate_url": "https://www.amazon.de/dp/B0CXTEST40?tag=padelnomics-21",
"price_cents": 5999,
"rating": 4.4,
"pros": '["Platz für 2 Schläger", "Gepolstertes Schlägerfach", "Robustes Material"]',
"cons": '["Kein Schuhfach"]',
"description": "Praktische Padelschläger-Tasche mit Platz für 2 Schläger und Zubehör.",
"status": "active",
"language": "de",
"sort_order": 1,
},
]
# Article slugs for realistic click referrers
_ARTICLE_SLUGS = [
"beste-padelschlaeger-2026",
"padelschlaeger-anfaenger",
"padelschuhe-test",
"padelbaelle-vergleich",
"padel-zubehoer",
]
def main(): def main():
db_path = DATABASE_PATH db_path = DATABASE_PATH
if not Path(db_path).exists(): if not Path(db_path).exists():
@@ -481,6 +659,72 @@ def main():
) )
logger.info(" PadelTech unlocked lead #%s", lead_id) logger.info(" PadelTech unlocked lead #%s", lead_id)
# 7. Seed affiliate products
logger.info("Seeding %s affiliate products...", len(AFFILIATE_PRODUCTS))
product_ids: dict[str, int] = {}
for p in AFFILIATE_PRODUCTS:
existing = conn.execute(
"SELECT id FROM affiliate_products WHERE slug = ? AND language = ?",
(p["slug"], p["language"]),
).fetchone()
if existing:
product_ids[p["slug"]] = existing["id"]
logger.info(" %s already exists (id=%s)", p["name"], existing["id"])
continue
cursor = conn.execute(
"""INSERT INTO affiliate_products
(slug, name, brand, category, retailer, affiliate_url,
price_cents, currency, rating, pros, cons, description,
status, language, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, 'EUR', ?, ?, ?, ?, ?, ?, ?)""",
(
p["slug"], p["name"], p["brand"], p["category"], p["retailer"],
p["affiliate_url"], p["price_cents"], p["rating"],
p["pros"], p["cons"], p["description"],
p["status"], p["language"], p["sort_order"],
),
)
product_ids[p["slug"]] = cursor.lastrowid
logger.info(" %s -> id=%s (%s)", p["name"], cursor.lastrowid, p["status"])
# 8. Seed affiliate clicks (realistic 30-day spread for dashboard charts)
logger.info("Seeding affiliate clicks...")
import random
rng = random.Random(42)
# click distribution: more on popular rackets, fewer on accessories
click_weights = [
("bullpadel-vertex-04-amazon", "beste-padelschlaeger-2026", 52),
("adidas-metalbone-30-amazon", "beste-padelschlaeger-2026", 41),
("head-delta-pro-amazon", "padelschlaeger-anfaenger", 38),
("wilson-bela-pro-v2-amazon", "padelschlaeger-anfaenger", 29),
("adidas-adipower-ctrl-amazon", "padelschuhe-test", 24),
("babolat-jet-premura-amazon", "padelschuhe-test", 18),
("head-padel-pro-balls-amazon", "padelbaelle-vergleich", 15),
("bullpadel-overgrip-3er-amazon", "padel-zubehoer", 11),
("nox-padel-bag-amazon", "padel-zubehoer", 8),
]
existing_click_count = conn.execute("SELECT COUNT(*) FROM affiliate_clicks").fetchone()[0]
if existing_click_count == 0:
for slug, article_slug, count in click_weights:
pid = product_ids.get(slug)
if not pid:
continue
for _ in range(count):
days_ago = rng.randint(0, 29)
hours_ago = rng.randint(0, 23)
clicked_at = (now - timedelta(days=days_ago, hours=hours_ago)).strftime("%Y-%m-%d %H:%M:%S")
ip_hash = f"dev_{slug}_{_:04d}" # stable fake hash (not real SHA256)
conn.execute(
"""INSERT INTO affiliate_clicks
(product_id, article_slug, referrer, ip_hash, clicked_at)
VALUES (?, ?, ?, ?, ?)""",
(pid, article_slug, f"https://padelnomics.io/de/blog/{article_slug}", ip_hash, clicked_at),
)
total_clicks = sum(c for _, _, c in click_weights)
logger.info(" Inserted %s click events across 9 products", total_clicks)
else:
logger.info(" Clicks already seeded (%s rows), skipping", existing_click_count)
conn.commit() conn.commit()
conn.close() conn.close()