#!/usr/bin/env python3
"""
Generuje raport HTML z danych JSON (Google Ads + GA4 + e-commerce + Semstorm).
Użycie:
python scripts/reports/generate_html_report.py --data output/innsi.pl_2026-02.json --client "INNSI"
"""
import argparse
import json
import re
import sys
import io
from pathlib import Path
from urllib.parse import urlparse
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
ROOT = Path(__file__).parent.parent.parent
TEMPLATES = Path(__file__).parent / "templates"
def generate_summary(data):
"""Generate positive-tone summary text based on data."""
ads = data["google_ads"]
mom = ads["mom_change"]
totals = ads["totals"]
ga4 = data.get("ga4")
ecom = ga4.get("ecommerce") if ga4 else None
lines = []
# E-commerce: revenue & transactions
if ecom and ecom.get("current"):
ec = ecom["current"]
ec_mom = ecom.get("mom_change", {})
revenue = ec["revenue"]
transactions = ec["transactions"]
aov = ec["aov"]
if (ec_mom.get("revenue_pct") or 0) > 5:
lines.append(
f"Przychód sklepu wyniósł {revenue:,.2f} PLN "
f"(+{ec_mom['revenue_pct']}% vs poprzedni miesiąc)."
)
elif (ec_mom.get("revenue_pct") or 0) > -5:
lines.append(f"Przychód sklepu utrzymał się na poziomie {revenue:,.2f} PLN.")
else:
lines.append(f"Przychód sklepu wyniósł {revenue:,.2f} PLN.")
lines.append(f"Zrealizowano {transactions} transakcji przy średniej wartości zamówienia {aov:.2f} PLN.")
# ROAS from Google Ads conversion_value
gads_roas = totals.get("roas", 0)
if gads_roas > 0:
lines.append(f"ROAS z Google Ads: {gads_roas:.2f} (każda wydana złotówka przyniosła {gads_roas:.2f} PLN przychodu).")
else:
# Non-ecommerce: conversions focus
if mom["conversions_pct"] > 10:
lines.append(
f"Liczba konwersji (telefonów) wzrosła o "
f"{mom['conversions_pct']}% w porównaniu "
f"do poprzedniego miesiąca ({int(totals['conversions'])} vs "
f"{int(ads['prev_totals']['conversions'])})."
)
elif mom["conversions_pct"] > 0:
lines.append(f"Konwersje utrzymały tendencję wzrostową ({int(totals['conversions'])} telefonów).")
elif mom["conversions_pct"] == 0:
lines.append(f"Liczba konwersji utrzymała się na stabilnym poziomie ({int(totals['conversions'])}).")
else:
lines.append(f"Odnotowano {int(totals['conversions'])} konwersji w tym miesiącu.")
# CPA
if mom["cpa_pct"] < -10:
lines.append(f"Koszt pozyskania klienta (CPA) spadł o {abs(mom['cpa_pct'])}% do {totals['cpa']:.2f} PLN — to bardzo dobry wynik.")
elif mom["cpa_pct"] < 0:
lines.append(f"CPA spadło do {totals['cpa']:.2f} PLN, co oznacza bardziej efektywne wydawanie budżetu.")
# CPC
if mom["cpc_pct"] < 0:
lines.append(f"Średni koszt kliknięcia (CPC) spadł o {abs(mom['cpc_pct'])}% do {totals['cpc']:.2f} PLN.")
# Clicks
if mom["clicks_pct"] > 0:
lines.append(f"Ruch z reklam wzrósł o {mom['clicks_pct']}% ({totals['clicks']} kliknięć).")
elif mom["clicks_pct"] > -5:
lines.append(f"Ruch z reklam utrzymał się na stabilnym poziomie ({totals['clicks']} kliknięć).")
# GA4
if ga4 and ga4.get("current"):
ga4_cur = ga4["current"]
ga4_mom = ga4.get("mom_change", {})
sessions = ga4_cur["sessions"]
users = ga4_cur["users"]
if ga4_mom.get("sessions_pct", 0) > 0:
lines.append(f"Ruch na stronie wzrósł: {sessions:,} sesji (+{ga4_mom['sessions_pct']}%), {users:,} użytkowników.")
else:
lines.append(f"Strona odnotowała {sessions:,} sesji i {users:,} użytkowników.")
# Semstorm
semstorm = data.get("semstorm")
if semstorm and semstorm.get("current"):
sc = semstorm["current"]
sm = semstorm.get("mom_change", {})
if sm.get("top10_pct", 0) > 0:
lines.append(f"Widoczność SEO w TOP10 wzrosła o {sm['top10_pct']}% ({sc['top10']} fraz).")
else:
lines.append(f"Widoczność SEO: {sc['top3']} fraz w TOP3, {sc['top10']} w TOP10.")
return " ".join(lines)
def format_number(n, decimals=0):
"""Format number with space as thousands separator."""
if decimals == 0:
return f"{int(n):,}".replace(",", " ")
return f"{n:,.{decimals}f}".replace(",", " ")
def build_html(data):
"""Build complete HTML report from data."""
ads = data["google_ads"]
totals = ads["totals"]
prev_totals = ads["prev_totals"]
mom = ads["mom_change"]
ga4 = data.get("ga4")
ecom = ga4.get("ecommerce") if ga4 else None
semstorm = data.get("semstorm")
seo_links = data.get("seo_links", [])
has_ecommerce = ecom and ecom.get("current")
has_semstorm = semstorm and semstorm.get("current")
has_seo_links = len(seo_links) > 0
client_name = data.get("client_display_name", data["client"])
month_name = data["month_name"]
year = data["year"]
prev_month_name = data["prev_month_name"]
summary_text = generate_summary(data)
recommendations = data.get("recommendations", [])
client_questions = data.get("client_questions", [])
# Filter out campaigns with 0 clicks
active_campaigns = [c for c in ads["campaigns"] if c["clicks"] > 0]
# Build KPI cards
def kpi_card(label, value, unit, change_pct, inverse=False):
"""inverse=True means decrease is good (cost, CPA)."""
if change_pct is None:
return f"""
"""
if change_pct > 0:
arrow = "▲" # ▲
color = "#e74c3c" if inverse else "#27ae60"
sign = "+"
elif change_pct < 0:
arrow = "▼" # ▼
color = "#27ae60" if inverse else "#e74c3c"
sign = ""
else:
arrow = "●" # ●
color = "#95a5a6"
sign = ""
return f"""
{label}
{value}{unit}
{arrow} {sign}{change_pct}% vs {prev_month_name}
"""
kpi_html = ""
kpi_html += kpi_card("Wyświetlenia", format_number(totals["impressions"]), "", mom["impressions_pct"])
kpi_html += kpi_card("Kliknięcia", format_number(totals["clicks"]), "", mom["clicks_pct"])
kpi_html += kpi_card("CTR", f'{totals["ctr"]:.1f}', "%", mom["ctr_pct"])
kpi_html += kpi_card("Konwersje", format_number(totals["conversions"]), "", mom["conversions_pct"])
kpi_html += kpi_card(
"Wartość konwersji",
format_number(totals.get("conversion_value", 0), 2),
" PLN",
mom.get("conversion_value_pct", 0),
)
kpi_html += kpi_card("Koszt", f'{totals["cost"]:.2f}', " PLN", mom["cost_pct"], inverse=True)
# ROAS from Google Ads conversion_value (not Shoper revenue)
gads_roas = totals.get("roas", 0)
prev_gads_roas = prev_totals.get("roas", 0)
if gads_roas > 0:
roas_change = mom.get("roas_pct")
if roas_change is None:
roas_change = round((gads_roas - prev_gads_roas) / prev_gads_roas * 100, 1) if prev_gads_roas > 0 else 0
kpi_html += kpi_card("ROAS", f'{gads_roas:.2f}', "x", roas_change)
# Campaign table rows
campaign_rows = ""
for c in active_campaigns:
campaign_rows += f"""
| {c["name"]} |
{c["type"]} |
{format_number(c["impressions"])} |
{format_number(c["clicks"])} |
{c["ctr"]:.1f}% |
{c["conversions"]:.0f} |
{format_number(c.get("conversion_value", 0), 2)} PLN |
{c["cost"]:.2f} PLN |
{c["cpa"]:.2f} PLN |
"""
# Search terms table
search_rows = ""
for i, t in enumerate(ads["search_terms"][:15], 1):
search_rows += f"""
| {i} |
{t["term"]} |
{format_number(t["impressions"])} |
{t["clicks"]} |
{t["ctr"]:.1f}% |
{t["conversions"]:.0f} |
"""
# Daily chart data
daily_labels = json.dumps([d["date"][5:] for d in ads["daily"]]) # MM-DD
daily_clicks = json.dumps([d["clicks"] for d in ads["daily"]])
daily_impressions = json.dumps([d["impressions"] for d in ads["daily"]])
# --- E-COMMERCE SECTION ---
ecom_section = ""
ecom_chart_js = ""
sales_history_chart_js = ""
if has_ecommerce:
ec = ecom["current"]
ec_mom = ecom.get("mom_change", {})
ecom_kpis = ""
ecom_kpis += kpi_card("Transakcje", format_number(ec["transactions"]), "", ec_mom.get("transactions_pct", 0))
ecom_kpis += kpi_card("Przychód", format_number(ec["revenue"], 2), " PLN", ec_mom.get("revenue_pct", 0))
ecom_kpis += kpi_card("Śr. wartość zamówienia", f'{ec["aov"]:.2f}', " PLN", ec_mom.get("aov_pct", 0))
# Revenue by source table
rbs = ecom.get("revenue_by_source", [])
rev_source_rows = ""
total_rev = ec["revenue"]
for rs in rbs:
pct = round(rs["revenue"] / total_rev * 100, 1) if total_rev else 0
bar_width = min(pct * 2, 100)
rev_source_rows += f"""
| {rs["source_medium"]} |
{format_number(rs["revenue"], 2)} PLN |
{rs["transactions"]} |
{pct}% |
|
"""
# Top products table
top_products = ecom.get("top_products", [])
top_products_rows = ""
for i, p in enumerate(top_products[:10], 1):
top_products_rows += f"""
| {i} |
{p["name"]} |
{p["quantity"]} |
{format_number(p["revenue"], 2)} PLN |
"""
top_products_html = ""
if top_products_rows:
top_products_html = f"""
Top 10 produktów
| # |
Produkt |
Ilość |
Przychód |
{top_products_rows}
"""
# Daily revenue chart data
ecom_daily = ecom.get("daily", [])
ecom_daily_labels = json.dumps([d["date"][5:] for d in ecom_daily])
ecom_daily_revenue = json.dumps([d["revenue"] for d in ecom_daily])
ecom_daily_transactions = json.dumps([d["transactions"] for d in ecom_daily])
ecom_chart_js = f"""
var ctxEcom = document.getElementById('ecomDailyChart').getContext('2d');
new Chart(ctxEcom, {{
type: 'bar',
data: {{
labels: {ecom_daily_labels},
datasets: [{{
label: 'Przychód (PLN)',
data: {ecom_daily_revenue},
backgroundColor: 'rgba(39, 174, 96, 0.6)',
borderColor: '#27ae60',
borderWidth: 1,
yAxisID: 'y',
order: 2,
}}, {{
label: 'Transakcje',
data: {ecom_daily_transactions},
type: 'line',
borderColor: '#8e44ad',
backgroundColor: 'transparent',
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#8e44ad',
yAxisID: 'y1',
order: 1,
}}]
}},
options: {{
responsive: true,
interaction: {{ mode: 'index', intersect: false }},
scales: {{
y: {{ beginAtZero: true, position: 'left', grid: {{ color: '#f0f0f0' }},
ticks: {{ callback: function(v) {{ return v.toLocaleString('pl-PL') + ' PLN'; }} }} }},
y1: {{ beginAtZero: true, position: 'right', grid: {{ display: false }} }},
x: {{ grid: {{ display: false }}, ticks: {{ maxTicksLimit: 10 }} }}
}}
}}
}});"""
# Sales history chart
sales_history = data.get("sales_history", [])
sales_history_html = ""
if len(sales_history) >= 1:
sh_labels = json.dumps([h["month"] for h in sales_history])
sh_revenue = json.dumps([h["revenue"] for h in sales_history])
sh_transactions = json.dumps([h["transactions"] for h in sales_history])
sales_history_chart_js = f"""
var ctxSalesHist = document.getElementById('salesHistoryChart').getContext('2d');
new Chart(ctxSalesHist, {{
type: 'bar',
data: {{
labels: {sh_labels},
datasets: [{{
label: 'Przychód (PLN)',
data: {sh_revenue},
backgroundColor: 'rgba(39, 174, 96, 0.5)',
borderColor: '#27ae60',
borderWidth: 1,
yAxisID: 'y',
order: 2,
}}, {{
label: 'Zamówienia',
data: {sh_transactions},
type: 'line',
borderColor: '#8e44ad',
backgroundColor: 'transparent',
tension: 0.3,
pointRadius: 4,
pointBackgroundColor: '#8e44ad',
yAxisID: 'y1',
order: 1,
}}]
}},
options: {{
responsive: true,
interaction: {{ mode: 'index', intersect: false }},
scales: {{
y: {{ beginAtZero: true, position: 'left', grid: {{ color: '#f0f0f0' }},
ticks: {{ callback: function(v) {{ return v.toLocaleString('pl-PL') + ' PLN'; }} }} }},
y1: {{ beginAtZero: true, position: 'right', grid: {{ display: false }} }},
x: {{ grid: {{ display: false }} }}
}}
}}
}});"""
sales_history_html = """
Historia sprzedaży (miesięcznie)
"""
daily_chart_html = ""
if ecom_daily:
daily_chart_html = """
Przychody i transakcje dziennie
"""
else:
ecom_chart_js = ""
rev_source_html = ""
if rev_source_rows:
rev_source_html = f"""
Ta tabela pochodzi z GA4 i pokazuje przychód według source / medium.
W sekcji Google Ads pokazujemy wartość konwersji z Google Ads, liczoną według atrybucji i okna konwersji Google Ads.
Przychody wg źródeł ruchu
| Źródło / Medium |
Przychód |
Transakcje |
Udział |
|
{rev_source_rows}
"""
ecom_section = f"""
E-commerce — Sprzedaż
{ecom_kpis}
{daily_chart_html}
{sales_history_html}
{rev_source_html}
{top_products_html}
"""
# --- PRODUCT OPTIMIZATIONS SECTION (e-commerce, globalna) ---
product_opts_section = ""
product_opts = data.get("product_optimizations", [])
if product_opts:
prod_rows = ""
for i, p in enumerate(product_opts, 1):
title_old = p.get("title_old", "") or "—"
title_new = p.get("title_new", "") or "—"
category = p.get("category", "") or "—"
unit_price = p.get("unit_price", "") or "—"
prod_rows += f"""
| {i} |
{title_old} {title_new} |
{category} |
{unit_price} |
"""
product_opts_section = f"""
Optymalizacja produktów
W tym miesiącu zoptymalizowaliśmy {len(product_opts)} produktów: tytuły reklamowe, kategorie Google oraz jednostki sprzedaży (unit price). Lepiej dopasowany feed = wyższa trafność, lepszy CTR i niższy CPC w kampaniach zakupowych.
| # |
Zmiana tytułu |
Kategoria Google |
Unit price |
{prod_rows}
"""
# --- TOP PRODUCTS (Google Ads) ---
top_ads_products_section = ""
top_ads_products = data.get("top_ads_products", [])
if top_ads_products:
rows = ""
for i, p in enumerate(top_ads_products[:10], 1):
roas = (p["value"] / p["cost"]) if p.get("cost") else 0
rows += f"""
| {i} |
{p["title"]} |
{format_number(p["clicks"])} |
{p["cost"]:.2f} PLN |
{p["conv"]:.0f} |
{format_number(p["value"], 2)} PLN |
{roas:.2f}x |
"""
top_ads_products_section = f"""
Top produkty — Google Ads
Produkty, które wygenerowały najwyższy przychód z kampanii zakupowych w {month_name} {year}. Ranking po wartości konwersji.
| # |
Produkt |
Kliknięcia |
Koszt |
Konwersje |
Wartość konwersji |
ROAS |
{rows}
"""
# --- YEAR OVER YEAR COMPARISON ---
yoy_section = ""
yoy = data.get("yoy", {})
if yoy and yoy.get("prev_year"):
py = yoy["prev_year"]
prev_year_label = yoy.get("prev_year_label", "rok temu")
def _yoy_row(label, cur, prev, fmt="num", inverse=False):
if prev == 0:
pct_html = "—"
else:
pct = round((cur - prev) / prev * 100, 1)
if pct > 0:
color = "#e74c3c" if inverse else "#27ae60"
pct_html = f'▲ +{pct}%'
elif pct < 0:
color = "#27ae60" if inverse else "#e74c3c"
pct_html = f'▼ {pct}%'
else:
pct_html = '● 0%'
if fmt == "money":
cur_s = f"{cur:,.2f} PLN".replace(",", " ")
prev_s = f"{prev:,.2f} PLN".replace(",", " ")
elif fmt == "ratio":
cur_s = f"{cur:.2f}x"
prev_s = f"{prev:.2f}x"
else:
cur_s = format_number(cur)
prev_s = format_number(prev)
return f"""| {label} | {prev_s} | {cur_s} | {pct_html} |
"""
cur_cost = totals["cost"]; prev_cost = py["cost"]
cur_conv = totals["conversions"]; prev_conv = py["conversions"]
cur_val = totals["conversion_value"]; prev_val = py["conversion_value"]
cur_roas = totals.get("roas", 0); prev_roas = (py["conversion_value"] / py["cost"]) if py.get("cost") else 0
cur_cpa = totals.get("cpa", 0); prev_cpa = (py["cost"] / py["conversions"]) if py.get("conversions") else 0
yoy_rows = ""
yoy_rows += _yoy_row("Wyświetlenia", totals["impressions"], py["impressions"])
yoy_rows += _yoy_row("Kliknięcia", totals["clicks"], py["clicks"])
yoy_rows += _yoy_row("Koszt", cur_cost, prev_cost, "money", inverse=True)
yoy_rows += _yoy_row("Konwersje", cur_conv, prev_conv)
yoy_rows += _yoy_row("Wartość konwersji", cur_val, prev_val, "money")
yoy_rows += _yoy_row("CPA", cur_cpa, prev_cpa, "money", inverse=True)
yoy_rows += _yoy_row("ROAS", cur_roas, prev_roas, "ratio")
yoy_section = f"""
Porównanie rok do roku
{prev_year_label} vs {month_name} {year} — jak rośnie skala działań i obrót generowany przez Google Ads.
| Wskaźnik |
{prev_year_label} |
{month_name} {year} |
Zmiana YoY |
{yoy_rows}
"""
# --- NEGATIVE KEYWORDS ADDED ---
negatives_section = ""
negatives = data.get("negative_keywords_added", [])
if negatives:
neg_rows = ""
for i, n in enumerate(negatives, 1):
neg_rows += f"""
| {i} |
{n.get("campaign","")} |
{n.get("keyword","")} |
{n.get("match_type","")} |
{n.get("reason","")} |
"""
negatives_section = f"""
Wykluczenia fraz w {month_name} {year}
Frazy wykluczone w tym miesiącu — każda z nich oznacza, że budżet nie jest już marnowany na ruch, który nie konwertuje. Łącznie dodano {len(negatives)} wykluczeń.
| # |
Kampania |
Fraza |
Typ dopasowania |
Powód |
{neg_rows}
"""
# --- GA4 SECTION ---
ga4_section = ""
ga4_chart_js = ""
if ga4 and ga4.get("current"):
ga4_cur = ga4["current"]
ga4_mom = ga4.get("mom_change", {})
ga4_kpis = ""
ga4_kpis += kpi_card("Sesje", format_number(ga4_cur["sessions"]), "", ga4_mom.get("sessions_pct", 0))
ga4_kpis += kpi_card("Użytkownicy", format_number(ga4_cur["users"]), "", ga4_mom.get("users_pct", 0))
ga4_kpis += kpi_card("Nowi użytkownicy", format_number(ga4_cur["new_users"]), "", ga4_mom.get("new_users_pct", 0))
ga4_kpis += kpi_card("Odsłon", format_number(ga4_cur["pageviews"]), "", ga4_mom.get("pageviews_pct", 0))
# Sources table
sources_rows = ""
total_sessions = sum(s["sessions"] for s in ga4.get("sources", []))
for s in ga4.get("sources", []):
pct = round(s["sessions"] / total_sessions * 100, 1) if total_sessions else 0
bar_width = min(pct * 2, 100)
sources_rows += f"""
| {s["source_medium"]} |
{format_number(s["sessions"])} |
{pct}% |
|
"""
# Devices table
devices_rows = ""
device_labels_pl = {"desktop": "Komputer", "mobile": "Telefon", "tablet": "Tablet"}
for d in ga4.get("devices", []):
pct = round(d["sessions"] / total_sessions * 100, 1) if total_sessions else 0
devices_rows += f"""
| {device_labels_pl.get(d["device"], d["device"])} |
{format_number(d["sessions"])} |
{pct}% |
"""
# GA4 daily chart data
ga4_daily = ga4.get("daily", [])
ga4_daily_labels = json.dumps([d["date"][5:] for d in ga4_daily])
ga4_daily_sessions = json.dumps([d["sessions"] for d in ga4_daily])
ga4_chart_js = f"""
var ctx3 = document.getElementById('ga4DailyChart').getContext('2d');
new Chart(ctx3, {{
type: 'line',
data: {{
labels: {ga4_daily_labels},
datasets: [{{
label: 'Sesje',
data: {ga4_daily_sessions},
borderColor: '#3498db',
backgroundColor: 'rgba(52,152,219,0.1)',
fill: true,
tension: 0.3,
pointRadius: 2,
}}]
}},
options: {{
responsive: true,
plugins: {{ legend: {{ display: false }} }},
scales: {{
y: {{ beginAtZero: true, grid: {{ color: '#f0f0f0' }} }},
x: {{ grid: {{ display: false }}, ticks: {{ maxTicksLimit: 10 }} }}
}}
}}
}});"""
ga4_section = f"""
Dane z Google Analytics
{ga4_kpis}
Sesje dziennie
Źródła ruchu
| Źródło / Medium | Sesje | Udział | |
{sources_rows}
Urządzenia
| Urządzenie | Sesje | Udział |
{devices_rows}
"""
# --- SEMSTORM SEO SECTION ---
semstorm_section = ""
semstorm_chart_js = ""
if has_semstorm:
sc = semstorm["current"]
sm = semstorm.get("mom_change", {})
sem_kpis = ""
sem_kpis += kpi_card("TOP 3", format_number(sc["top3"]), " fraz", sm.get("top3_pct", 0))
sem_kpis += kpi_card("TOP 10", format_number(sc["top10"]), " fraz", sm.get("top10_pct", 0))
sem_kpis += kpi_card("TOP 50", format_number(sc["top50"]), " fraz", sm.get("top50_pct", 0))
sem_kpis += kpi_card("Szac. ruch SEO", format_number(sc["traffic"]), "", sm.get("traffic_pct", 0))
# History chart
history = semstorm.get("history", [])
if history:
hist_labels = json.dumps([h["month"] for h in history])
hist_top3 = json.dumps([h["top3"] for h in history])
hist_top10 = json.dumps([h["top10"] for h in history])
hist_top50 = json.dumps([h["top50"] for h in history])
semstorm_chart_js = f"""
var ctxSem = document.getElementById('semstormChart').getContext('2d');
new Chart(ctxSem, {{
type: 'line',
data: {{
labels: {hist_labels},
datasets: [{{
label: 'TOP 3',
data: {hist_top3},
borderColor: '#27ae60',
backgroundColor: 'rgba(39,174,96,0.1)',
fill: true,
tension: 0.3,
pointRadius: 4,
pointBackgroundColor: '#27ae60',
}}, {{
label: 'TOP 10',
data: {hist_top10},
borderColor: '#2980b9',
backgroundColor: 'transparent',
tension: 0.3,
pointRadius: 4,
pointBackgroundColor: '#2980b9',
}}, {{
label: 'TOP 50',
data: {hist_top50},
borderColor: '#95a5a6',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.3,
pointRadius: 3,
yAxisID: 'y1',
}}]
}},
options: {{
responsive: true,
interaction: {{ mode: 'index', intersect: false }},
scales: {{
y: {{ beginAtZero: false, position: 'left', grid: {{ color: '#f0f0f0' }},
title: {{ display: true, text: 'TOP 3 / TOP 10' }} }},
y1: {{ beginAtZero: false, position: 'right', grid: {{ display: false }},
title: {{ display: true, text: 'TOP 50' }} }},
x: {{ grid: {{ display: false }} }}
}}
}}
}});"""
semstorm_section = f"""
SEO — Widoczność (Semstorm)
{sem_kpis}
{"" if not history else '''
Historia widoczności
'''}
"""
# --- SEO ACTIVITIES BOX ---
seo_activities_section = ""
seo_activities_text = data.get("seo_activities", "")
if seo_activities_text:
# Convert URLs in text to clickable links
def _linkify(text):
return re.sub(
r'(https?://\S+)',
r'\1',
text
)
# Convert newlines to
formatted = _linkify(seo_activities_text.replace("\n", "
\n"))
seo_activities_section = f"""
SEO — Pozostałe działania
{formatted}
"""
# --- SEO LINKS SECTION ---
seo_links_section = ""
if has_seo_links:
# Extract all URLs — either from individual entries or from text descriptions
all_urls = []
for lnk in seo_links:
url_val = lnk["url"].strip()
parsed_check = urlparse(url_val)
if parsed_check.scheme in ("http", "https") and "\n" not in url_val and len(url_val) < 300:
# Clean single URL
all_urls.append(url_val)
else:
# Text with embedded URLs — extract them
found = re.findall(r'https?://[^\s,\r\n]+', url_val)
all_urls.extend(found)
if all_urls:
link_rows = ""
for i, url in enumerate(all_urls, 1):
parsed = urlparse(url)
display_domain = parsed.netloc
link_rows += f"""
| {i} |
{display_domain} |
{url} |
"""
seo_links_section = f"""
SEO — Pozyskane linki ({len(all_urls)})
"""
# Full HTML
html = f"""
Raport {month_name} {year} — {client_name}
{"" if not recommendations else '''
Wnioski i rekomendacje
''' + "".join(f'
' for r in recommendations) + '''
'''}
{"" if not client_questions else '''
Pytania do Pana
Aby lepiej zaplanować dalsze działania, chcielibyśmy poznać Pana zdanie w kilku kwestiach:
''' + "".join(f'
' for i, q in enumerate(client_questions)) + '''
'''}
{ga4_section}
{ecom_section}
{product_opts_section}
{top_ads_products_section}
{yoy_section}
Google Ads — Podsumowanie
{kpi_html}
Google Ads — Aktywność dzienna
Kampanie
| Kampania |
Typ |
Wyświetlenia |
Kliknięcia |
CTR |
Konwersje |
Wartość konwersji |
Koszt |
CPA |
{campaign_rows}
Najpopularniejsze frazy wyszukiwania
| # |
Fraza |
Wyświetlenia |
Kliknięcia |
CTR |
Konwersje |
{search_rows}
{negatives_section}
{semstorm_section}
{seo_activities_section}
{seo_links_section}
Podsumowanie miesiąca
{summary_text}
"""
return html
def main():
parser = argparse.ArgumentParser(description="Generuj raport HTML")
parser.add_argument("--data", required=True, help="Sciezka do pliku JSON z danymi")
parser.add_argument("--client", default=None, help="Nazwa klienta do wyswietlenia (np. 'INNSI')")
parser.add_argument("--output", help="Sciezka do pliku wyjsciowego")
args = parser.parse_args()
data_path = Path(args.data)
if not data_path.is_absolute():
data_path = ROOT / "scripts" / "reports" / data_path
with open(data_path, "r", encoding="utf-8") as f:
data = json.load(f)
if args.client:
data["client_display_name"] = args.client
html = build_html(data)
if args.output:
out_path = Path(args.output)
else:
domain = data["client"].replace(".", "-")
out_dir = ROOT / "scripts" / "reports" / "output" / data["client"] / data["month"]
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / "index.html"
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write(html)
print(f"Raport wygenerowany: {out_path}")
if __name__ == "__main__":
main()