#!/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"""
{label}
{value}{unit}
 
""" 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

{top_products_rows}
# Produkt Ilość Przychód
""" # 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

{rev_source_rows}
Źródło / Medium Przychód Transakcje Udział
""" 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.

{prod_rows}
# Zmiana tytułu Kategoria Google Unit price
""" # --- 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.

{rows}
# Produkt Kliknięcia Koszt Konwersje Wartość konwersji ROAS
""" # --- 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.

{yoy_rows}
Wskaźnik {prev_year_label} {month_name} {year} Zmiana YoY
""" # --- 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ń.

{neg_rows}
# Kampania Fraza Typ dopasowania Powód
""" # --- 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

{sources_rows}
Źródło / MediumSesjeUdział

Urządzenia

{devices_rows}
UrządzenieSesjeUdział
""" # --- 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""" """ # Full HTML html = f""" Raport {month_name} {year} — {client_name}

Raport z działań marketingowych

{client_name} — {month_name} {year}
{"" if not recommendations else '''

Wnioski i rekomendacje

''' + "".join(f'
{r.get("icon", "➤")}
{r["title"]}

{r["text"]}

' 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'
{i+1}
{q["title"]}

{q["text"]}

' 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

{campaign_rows}
Kampania Typ Wyświetlenia Kliknięcia CTR Konwersje Wartość konwersji Koszt CPA

Najpopularniejsze frazy wyszukiwania

{search_rows}
# Fraza Wyświetlenia Kliknięcia CTR Konwersje
{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()