From 03e6d7287ea6751aabf2bb70a6aba2ccfe31061e Mon Sep 17 00:00:00 2001 From: beantownbabe <156287695+beantownbabe@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:22:40 +0100 Subject: [PATCH] Wire CTA Campaign phases into Reports as real computable periods CUSTOM_PERIODS in report.py now drives _filtered/_period_label/ compute_period_stats for the 3 CTA phases (date ranges from the proposed plan), and the campaign-report title treatment applies to them the same way it does for July-September. Removed the app.py placeholder guards (reports tab + docx download) now that real period computation exists instead of an alert box, and fixed the docx filename label lookup to check all campaigns' periods, not just the old single flat list. Added Core Message (reused) and Roadmap (per-phase) abstracts to monthly_report_notes.py for the 3 phases - no executive_summary yet since the campaign hasn't run and isn't approved, so KPI/chart cards render on real, currently-empty data rather than fabricated findings. --- app.py | 48 ++++++++++++++++------------------------- monthly_report_notes.py | 31 ++++++++++++++++++++++++++ report.py | 30 +++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/app.py b/app.py index 8c44f9d..028b335 100644 --- a/app.py +++ b/app.py @@ -2200,9 +2200,9 @@ _REPORTS_CAMPAIGNS = { }, "cta-campaign": { "label": "CTA Campaign", - # Placeholder periods only - dates from the proposed plan, not yet approved. - # Selecting one shows a "not built yet" placeholder (_render_reports_tab), - # same treatment "End of Campaign" itself got before it was built out. + # Dates from the proposed plan - campaign isn't approved yet, so these + # render as real (currently empty/near-zero) reports once the period + # starts, same as any other month would before it has data. "periods": [ ("cta-phase-1", "Nov 3 - Nov 26 (Failure Analysis)"), ("cta-phase-2", "Dec 1 - Jan 7 (Standardization)"), @@ -2212,6 +2212,10 @@ _REPORTS_CAMPAIGNS = { } _REPORTS_CAMPAIGN_OPTIONS = [{"label": v["label"], "value": k} for k, v in _REPORTS_CAMPAIGNS.items()] _REPORTS_DEFAULT_CAMPAIGN = "linkedin-branding" +# Flat lookup across every campaign's periods, for the PDF/docx filename label - +# the Period dropdown only ever holds one campaign's periods at a time (via the +# cascade callback), so this is the one place that needs all of them at once. +_REPORTS_ALL_PERIODS = [kv for camp in _REPORTS_CAMPAIGNS.values() for kv in camp["periods"]] reports_tab = dbc.Container([ html.Div([ @@ -4522,34 +4526,22 @@ def _render_reports_tab(month_value): if month_value == "campaign-end": return _render_campaign_end_summary() + (store_value,) - if month_value.startswith("cta-phase"): - # CTA Campaign is proposed, not yet approved — placeholder only, same - # treatment "End of Campaign" itself got before it was built out. - period_label = next((lbl for v, lbl in _REPORTS_CAMPAIGNS["cta-campaign"]["periods"] - if v == month_value), month_value) - title = html.H3(period_label, - style={"color": "#1b3a5c", "marginBottom": "0", "fontWeight": "700", - "fontSize": "2rem", "whiteSpace": "nowrap"}) - placeholder = dbc.Alert( - "The CTA Campaign hasn't been approved yet, so there's no report to show. " - "This period will build out the same way the other reports do once it's live.", - color="info") - return title, placeholder, no_update - stats = report.compute_period_stats(_build_report_ctx(), month_value) notes = monthly_report_notes.NOTES.get(month_value) fts_f, posts_f = stats["fts_f"], stats["posts_f"] - # Only July/August/September were part of the 3-month branding campaign — a - # later month like October gets a plain title, no active campaign stage + # Only July/August/September were part of the 3-month branding campaign, and + # CTA Campaign phases (CUSTOM_PERIODS) once that campaign is live — any other + # month like October gets a plain title, no active campaign stage # (goal_line/core_message/roadmap) behind it. - _is_campaign_month = False - try: - _year, _month = (int(p) for p in month_value.split("-")) - _month_start = pd.Timestamp(year=_year, month=_month, day=1) - _is_campaign_month = report.CAMPAIGN_START <= _month_start <= report.CAMPAIGN_END - except ValueError: - pass + _is_campaign_month = month_value in report.CUSTOM_PERIODS + if not _is_campaign_month: + try: + _year, _month = (int(p) for p in month_value.split("-")) + _month_start = pd.Timestamp(year=_year, month=_month, day=1) + _is_campaign_month = report.CAMPAIGN_START <= _month_start <= report.CAMPAIGN_END + except ValueError: + pass title_heading = html.H3( f"{stats['period_label']} End of Month Campaign Report" if _is_campaign_month else f"TXproducts — {stats['period_label']}", @@ -4770,12 +4762,10 @@ def _render_reports_tab(month_value): prevent_initial_call=True, ) def _download_reports_pdf(_n_clicks, month_value): - if month_value and month_value.startswith("cta-phase"): - return no_update # CTA Campaign not approved yet, nothing to download ctx_data = _build_report_ctx() ctx_data["monthly_notes"] = monthly_report_notes.NOTES.get(month_value) docx_bytes = report.build_report_docx(ctx_data, month_value) - label = next((o["label"] for o in _REPORTS_MONTH_OPTIONS if o["value"] == month_value), month_value) + label = next((lbl for v, lbl in _REPORTS_ALL_PERIODS if v == month_value), month_value) filename = f"TXproducts_LinkedIn_Report_{label.replace(' ', '_')}.docx" return dcc.send_bytes(lambda buf: buf.write(docx_bytes), filename=filename) diff --git a/monthly_report_notes.py b/monthly_report_notes.py index 4e8fd1a..ea6f80e 100644 --- a/monthly_report_notes.py +++ b/monthly_report_notes.py @@ -516,4 +516,35 @@ NOTES = { ], }, }, + # CTA Campaign phases (Nov 2026 - Feb 2027), pending approval. Core Message is + # the same constant every other campaign reuses (TXproducts' value proposition + # doesn't change); the Roadmap abstract is what changes per phase, drawn + # directly from the proposed plan's own theme framing. No executive_summary yet + # - that would mean reporting findings from a campaign that hasn't run, so the + # KPI table and charts render on real (currently empty) data instead until + # there's something real to write up, same as any other not-yet-run period. + "cta-phase-1": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("Failure Analysis.", True), + (" Power electronics fail under real operating stress, and time " + "resolution shows the failure as it happens.", False), + ], + }, + "cta-phase-2": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("Standardization.", True), + (" Failure results are only useful when they're comparable: same " + "stress, same measurement, same pipeline on every sample.", False), + ], + }, + "cta-phase-3": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("WaveGate.", True), + (" The technology that makes standardized, time-resolved failure " + "analysis possible.", False), + ], + }, } diff --git a/report.py b/report.py index 98e24e2..fc840cc 100644 --- a/report.py +++ b/report.py @@ -30,12 +30,30 @@ import engagement_audit CAMPAIGN_START = pd.Timestamp("2026-07-01") CAMPAIGN_END = pd.Timestamp("2026-09-30") +# Custom (non-calendar-month) reporting periods, keyed the same way the Reports +# dropdown keys them. "campaign-end" isn't listed here since it's handled directly +# above - everything after that is the CTA Campaign's 3 phases (dates from the +# proposed plan, pending approval). Each period is a plain date range with its own +# label, not tied to a calendar month, since the CTA Campaign's own phases span +# across month boundaries (Dec 1-Jan 7) on purpose, unlike July/August/September. +CUSTOM_PERIODS = { + "cta-phase-1": (pd.Timestamp("2026-11-03"), pd.Timestamp("2026-11-26"), + "Nov 3 - Nov 26 (Failure Analysis)"), + "cta-phase-2": (pd.Timestamp("2026-12-01"), pd.Timestamp("2027-01-07"), + "Dec 1 - Jan 7 (Standardization)"), + "cta-phase-3": (pd.Timestamp("2027-01-12"), pd.Timestamp("2027-02-04"), + "Jan 12 - Feb 4 (WaveGate)"), +} + def _filtered(df, date_col, value): if not value or value == "all": return df if value == "campaign-end": return df[(df[date_col] >= CAMPAIGN_START) & (df[date_col] <= CAMPAIGN_END)] + if value in CUSTOM_PERIODS: + start, end, _ = CUSTOM_PERIODS[value] + return df[(df[date_col] >= start) & (df[date_col] <= end)] if value.startswith("year-"): year = int(value.split("-", 1)[1]) return df[df[date_col].dt.year == year] @@ -51,6 +69,8 @@ def _period_label(value): return "All Time" if value == "campaign-end": return "3-Month Campaign (Jul-Sep 2026)" + if value in CUSTOM_PERIODS: + return CUSTOM_PERIODS[value][2] if value.startswith("year-"): return value.split("-", 1)[1] import calendar as calmod @@ -123,6 +143,8 @@ def compute_period_stats(ctx, timeframe_value="all"): first_day = None if timeframe_value == "campaign-end": first_day = CAMPAIGN_START + elif timeframe_value in CUSTOM_PERIODS: + first_day = CUSTOM_PERIODS[timeframe_value][0] elif timeframe_value and timeframe_value != "all" and not timeframe_value.startswith("year-"): year, month = (int(p) for p in timeframe_value.split("-")) first_day = pd.Timestamp(year=year, month=month, day=1) @@ -248,10 +270,12 @@ def build_report_docx(ctx, timeframe_value="all"): # Only July/August/September were part of the 3-month branding campaign # (CAMPAIGN_START-CAMPAIGN_END) — a later month like October gets a plain # title, not "...Campaign Report", since there's no active campaign stage - # (no goal_line/core_message/roadmap) behind it. - is_campaign_month = False + # (no goal_line/core_message/roadmap) behind it. CTA Campaign phases + # (CUSTOM_PERIODS) get the same campaign-report treatment once that + # campaign is approved and running. + is_campaign_month = timeframe_value in CUSTOM_PERIODS if timeframe_value and timeframe_value != "all" and not timeframe_value.startswith("year-") \ - and timeframe_value != "campaign-end": + and timeframe_value != "campaign-end" and timeframe_value not in CUSTOM_PERIODS: try: year, month = (int(p) for p in timeframe_value.split("-")) month_start = pd.Timestamp(year=year, month=month, day=1)