Add Executive Summary as the report's first page
A standalone first page ahead of any detailed numbers: the month's goal (from the Roadmap), a Yes/No/Partial verdict on whether it was met, and a Pros/Cons list, color-coded (green/red/amber). Hand- authored per month in monthly_report_notes.py's new "executive_summary" key, same pattern as the other narrative sections, since "did we meet our goal" is a judgment call, not something computed from the export data. Renders only when present, so August/September are unaffected until written up. A page break separates it from the rest of the report so it stands alone as page 1. Drafted July's as a first pass grounded in facts already established in this report (the emotional-vs-rational engagement gap, the poll's zero engagement, the 27/08 audit gap) — needs Mckinley's review since the goal-met verdict is inherently a judgment call, not something to assert unilaterally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
292bd21abe
commit
40c8ccc01e
2 files changed
+70
-1
No files matched your search
@@ -21,6 +21,38 @@ _CORE_MESSAGE_ABSTRACT = [
|
||||
|
||||
NOTES = {
|
||||
"2026-07": {
|
||||
"executive_summary": {
|
||||
"goal": "Find the Voice — establish a consistent brand voice and discover "
|
||||
"which posts and formats drove the most engagement.",
|
||||
"goal_met": "Yes",
|
||||
"goal_met_note": "A clear format/tone winner emerged: Carousels and a "
|
||||
"personal, emotional-register story (the 14/07-28/07-30/07 "
|
||||
"trilogy) outperformed everything else, giving a concrete, "
|
||||
"audited answer to what resonates with this audience.",
|
||||
"pros": [
|
||||
"Emotional/personal-voice posts averaged 21.8% Real Eng % vs. Rational "
|
||||
"posts' 8.1%, a wide, decisive gap.",
|
||||
"Page grew from 85 to 107 followers (+22, +25.9%).",
|
||||
"Best single post hit 45.0% Real Eng %, the clear #1 all month.",
|
||||
"Real Eng % audit methodology (Audience-only, hand-verified, Team "
|
||||
"excluded) built and rolled out, replacing LinkedIn's raw/Team-inflated "
|
||||
"numbers with a trustworthy figure everything else in this report reads "
|
||||
"from.",
|
||||
],
|
||||
"cons": [
|
||||
"The one validation attempt (21/07 poll) got zero responses — whether "
|
||||
"this audience engages with interactive formats at all is still "
|
||||
"unanswered.",
|
||||
"10/07 was a repost, not original content, requiring exclusion logic to "
|
||||
"keep every number honest — a reminder to plan the full month's content "
|
||||
"in advance.",
|
||||
"'Avg. Eng. Rate' (LinkedIn's raw daily figure) is still unreconciled "
|
||||
"with Real Eng %, an open methodology question.",
|
||||
"Audit coverage had a real gap (27/08) that wasn't caught until "
|
||||
"September — the hand-audit process needs to stay current within the "
|
||||
"month it covers, not catch up later.",
|
||||
],
|
||||
},
|
||||
"core_message_abstract": _CORE_MESSAGE_ABSTRACT,
|
||||
"roadmap_abstract": [
|
||||
("Find the Voice.", True),
|
||||
|
||||
@@ -21,7 +21,7 @@ from reportlab.lib.units import cm
|
||||
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle, Paragraph,
|
||||
Spacer, Image, HRFlowable)
|
||||
from docx import Document
|
||||
from docx.shared import Cm, Pt
|
||||
from docx.shared import Cm, Pt, RGBColor
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
@@ -897,6 +897,43 @@ def build_report_docx(ctx, timeframe_value="all"):
|
||||
f"Data as of {D['last_updated'].strftime('%B %d, %Y')}"
|
||||
).italic = True
|
||||
|
||||
# --- Executive Summary: standalone first page (goal, met/not, pros/cons) — a
|
||||
# leadership-facing read of "did this month work", ahead of any of the detailed
|
||||
# numbers below it. Hand-authored per month in monthly_report_notes.py, same as
|
||||
# the other narrative sections, since "did we meet our goal" is a judgment call,
|
||||
# not something computed from the export data.
|
||||
exec_summary = (monthly_notes or {}).get("executive_summary")
|
||||
if exec_summary:
|
||||
doc.add_heading("Executive Summary", level=1)
|
||||
goal_p = doc.add_paragraph()
|
||||
goal_p.add_run("Goal: ").bold = True
|
||||
goal_p.add_run(exec_summary.get("goal", ""))
|
||||
met_p = doc.add_paragraph()
|
||||
met_p.add_run("Goal Met: ").bold = True
|
||||
met_run = met_p.add_run(exec_summary.get("goal_met", "—"))
|
||||
met_run.bold = True
|
||||
goal_met_val = str(exec_summary.get("goal_met", "")).strip().lower()
|
||||
if goal_met_val == "yes":
|
||||
met_run.font.color.rgb = RGBColor(0x2E, 0x7D, 0x32)
|
||||
elif goal_met_val == "no":
|
||||
met_run.font.color.rgb = RGBColor(0xC6, 0x28, 0x28)
|
||||
elif goal_met_val == "partial":
|
||||
met_run.font.color.rgb = RGBColor(0xB2, 0x6A, 0x00)
|
||||
goal_met_note = exec_summary.get("goal_met_note")
|
||||
if goal_met_note:
|
||||
doc.add_paragraph(goal_met_note)
|
||||
pros = exec_summary.get("pros") or []
|
||||
if pros:
|
||||
doc.add_heading("Pros", level=2)
|
||||
for item in pros:
|
||||
doc.add_paragraph(item, style="List Bullet")
|
||||
cons = exec_summary.get("cons") or []
|
||||
if cons:
|
||||
doc.add_heading("Cons", level=2)
|
||||
for item in cons:
|
||||
doc.add_paragraph(item, style="List Bullet")
|
||||
doc.add_page_break()
|
||||
|
||||
# --- KPI summary table ---
|
||||
doc.add_heading("Key Metrics", level=1)
|
||||
kpi_rows = [
|
||||
|
||||
Reference in new issue
Block a user