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.
TXproducts stated the pain point in 08/09's post; the audience didn't
originate it, they confirmed it was real by engaging in discussion.
"Said" implied the audience brought it up themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follows the 13/08 Content Type fix (Carousel, not Text/Image).
Recomputed: Carousel now ran 7 times (not 6), averaging 29.1% (not
25.9%) - clearly the campaign's best format, not a close contest with
Text/Image (whose average drops to 9.1% once its one "strong" post
turns out to actually be a Carousel). 13/08 is the campaign's #2 post
at 48.7%, combining Carousel with a concrete cost claim.
This also corrects a now-wrong recommendation: "test the untried
Carousel + concrete claim combination" was framed as unexplored, but
13/08 already proved it works. The real remaining gap is narrower and
more specific - Carousel paired with a named pain point (September's
trigger finding), not just any concrete claim.
Mckinley confirmed this report (unlike the monthly ones) isn't
submitted yet, so updated directly rather than just flagging it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mckinley flagged the July/August stage bullets as confusing. Two
separate problems: July's bullet conflated Carousel's 4-post average
with the 14/07-28/07-30/07 trilogy as if they were the same finding,
when the trilogy is mostly a Video story (only 30/07 was Carousel).
And "Carousel's single best post of the campaign (50.8%)" in August's
bullet left it ambiguous whether 50.8% was an average or one post's
score (it's one post, 04/08 - August only had a single Carousel that
month, nothing to average).
Rewrote both to be explicit: July's now separates the format-average
finding from the narrative-arc finding into two sentences, and spells
out the trilogy's actual formats (Video, Video, Carousel). August's
now says directly "the single Carousel this month (04/08) was the
campaign's single best post."
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The same ~4 findings (Carousel's variance, audience stability, direct
asks failing, the untried Carousel+pain-point combination) were each
restated 3 times with near-identical wording across engagement_findings,
audience_dimensions, and recommendations — the opposite of the "simple,
not 8 pages" point of this report format.
Consolidated so each fact lives once, in its most natural place:
engagement_findings keeps the Carousel-variance and the one-trigger
stats (audience stability dropped, already fully covered by
audience_composition_line). trilogy_closing trimmed to the observation
only, the fix moved into recommendations. audience_dimensions rewritten
entirely — it was just restating stats already given elsewhere; now it's
the Prior Knowledge/Decision Context/Trust framing the monthly reports
already use, synthesized across the whole campaign (including a
genuinely new point: the audience's Decision Context shifted in
September, the first month with real budget-holding presence).
recommendations trimmed to action + one-clause justification instead
of re-deriving the full stat block each time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while cross-checking the user's own annotated PDF copies on
their Desktop against the live data. August's 06/08 Poll was recorded
as "a small but real response (5.2%)" in the executive summary, but
the actual audit (engagement_audit.py's own "06/08" entry, unchanged
since it was first audited) already had zero audience AND zero team
engagement from Likes/Comments/Reposts - the 5.2% was entirely
unaudited Clicks, unrelated to anyone actually engaging with the poll
itself. The real finding: both poll attempts (21/07, 06/08) got zero
genuine follower response, not an improvement month to month, and the
Poll/Wildcard format was removed from the campaign after the second
attempt - a real decision this file wasn't recording anywhere.
Fixed in August's own executive_summary and in the campaign-end
summary, which was missing August's poll attempt from its validation
narrative entirely (only mentioned July's).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
29/09's Carousel ("The two pillars of TXservices") is now in a fresh
export and audited: likes A1/T2 (Sabine Brock audience; Aleksandra
Kotenko, Mckinley Black team), 1 team comment, 3 team reposts ("the
usual suspects" - Aleksandra, Peter, Mckinley). Real Eng % 10.0%,
September's new growth/KPI numbers: 126->142 (+16, +12.7%).
This surfaces a real correction: July actually had 4 Carousels
(09/07, 16/07, 23/07, 30/07), not the 1 everyone's mental model had
been built around, ranging 6.0%-45.0%. Across all 6 Carousels run
this campaign (4 July + 1 August + 1 September) the format averages
25.9% Real Eng %, still the best of any format, but ranges 6.0% to
50.8% - not the clean "wins every time" story the campaign-end
summary told before this. Rewrote September's and campaign-end's
Executive Summaries throughout: the trilogy/stage narratives, the
best-posting-day findings (Tuesday's edge was never confounded by
format once September separated the variables), the recommendations
(pair Carousel with a named claim instead of trusting the format
alone, test the untried Carousel+pain-point combination).
Also fixed a conflation Mckinley flagged directly: July's poll (a
genuine validation test - does this audience respond to an
interactive format at all) and 17/09's pilot-partner ask (a direct
conversion CTA) are different kinds of things and were wrongly
grouped under one "validation" framing in both September's and the
campaign-end's summaries - now described as what they actually are.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "campaign-end" dropdown option existed but was a placeholder ("not
built yet") in the live tab and explicitly blocked in the download
callback. Built it out properly, same condensed Executive Summary
philosophy as the monthly reports, not a merge of all three:
- report.py: "campaign-end" is now a real timeframe_value throughout
_filtered/_period_label/compute_period_stats, scoped to the fixed
campaign window (CAMPAIGN_START/CAMPAIGN_END, 2026-07-01 to
2026-09-30) rather than "all" (which would pull in pre-campaign
June data). Its Posts Published excludes the repost and the one
non-campaign post, unlike a single month's own count, since a
capstone number should mean genuine campaign content. Monthly
Growth Rate anchors to the campaign's own start instead of a
calendar month's.
- monthly_report_notes.py: new NOTES["campaign-end"] executive_summary
synthesizing all three months — what each stage actually proved
(Carousel wins, a stable Academia/Research/Germany audience, one
real trigger found but not repeated), the campaign-wide best-posting
-day finding, and a new "recommendations" field: concrete, data-
grounded ideas for October and beyond (bring Carousel back, lead
with named pain points, move asks into direct outreach using the
21 already-engaged non-followers sitting in outreach_contacts.py,
track conversion as its own funnel, properly separate the day/
format confound next time).
- build_report_docx() renders "recommendations" as a new section when
present. app.py's download callback no longer blocks campaign-end,
and the live Reports tab gets a real (if intentionally simple)
preview via a new _render_campaign_end_summary(), built as its own
self-contained renderer rather than stretching the 7 per-month
insights pages (built around a single YYYY-MM, not a 3-month span)
to fit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Grounded in the actual per-post data (Real Eng % by post/format/
weekday) and post titles, not just the abstract pros/cons July
started with:
August (Find the Audience, Goal Met: Yes) — audience composition
answer (Academia/Research-heavy, Germany-heavy, Researcher/Scientist
seniority), the TXServices launch arc across 6 posts, Carousel
repeating July's win but a plain value-prop post (13/08) nearly
matching it, and the Tuesday/Thursday day-vs-format confound still
unresolved.
September (Find the Trigger, Goal Met: Partial) — 08/09's pain-point
post was the only one with real discussion and the month's best Real
Eng %, but that ceiling (18.75%) is the campaign's lowest yet, no
Carousel was tried all month, growth decelerated to the slowest pace
of the campaign (+11.1%), and the direct pilot-partner ask (17/09)
underperformed. Honest "Partial" verdict rather than forcing a Yes:
one trigger instance was found but not turned into a repeatable
pattern, and the numbers moved the wrong direction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mckinley hand-edited July's report down to a single page and showed
exactly what an Executive Summary should look like: the deep dive's
actual findings pulled straight into prose (which post drove
engagement, the 14/07-28/07-30/07 trilogy story, best posting day and
its format confound, audience composition's three dimensions), the
Key Metrics table flowing right after the goal/verdict with no extra
heading, one chart (Follower Growth), and nothing else. Confirmed
directly: this is the whole report now, not a summary page ahead of
a longer appendix — readers go to the live dashboard for every other
table/chart/register the old 8-page version duplicated.
report.build_report_docx() rewritten from scratch to this shape, now
reading everything from an expanded "executive_summary" dict in
monthly_report_notes.py (goal_line, goal_met, growth_note, engagement
findings, the trilogy, best-posting-day note + caveat, the validation
gap, audience composition line + dimensions) instead of the old
pros/cons list — the function itself is now a pure renderer, every
month-specific fact lives in monthly_report_notes.py same as every
other narrative section.
build_report_pdf() and its reportlab machinery deleted entirely
(genuinely dead code — the only call site switched to docx already),
along with every helper that existed only to serve it. report.py
drops from ~1260 lines to ~300. August and September don't have an
executive_summary yet, so their reports fall back to Key Metrics +
Follower Growth + a note that the summary isn't written up, rather
than silently reusing July's content or crashing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real Eng % is Mckinley's own per-post hand audit (name-level Likes/
Comments/Reposts, Team excluded one person at a time). Avg. Eng. Rate
is LinkedIn's own daily page-level aggregate with no name-level
breakdown at all, so there's nothing to audit it against and no way
to exclude Team from a single blended daily number the way it can be
excluded from one post's own Reactions list. That's not a pending
methodology question, it's just two genuinely different metrics, so
every "Avg. Eng. Rate" label (Overview, Roadmap insights, Reports tab
KPI row, PDF/docx tables) now reads "raw, unaudited" instead of
leaving the distinction implicit. Removes the now-resolved item from
July's Executive Summary cons and August's outstanding list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Adds the 4 missing sections (features_shipped, data_quality,
follower_study, outstanding) for August's End of Month report,
sourced from August's actual commit history (Reports tab build-out,
GOTO Messaging Handbook, Engagement Composition polish, Real Eng %/
CTR % formula callouts, follow-date corrections, the Mihail Petev
repost correction, git-tracking the Follower Directory for the first
time, and the 107-to-126 follower growth). August's audit data was
already complete; this was the only piece blocking its report.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was framed as a big "backfill full history or accept the gap" decision;
now that pre/non-campaign posts are properly excluded, the real remaining
gap is just one missed post (27/08) plus the newest post not yet sent over
(08/09) — never a large historical question.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Decided against the 3-7 day match-window/weekly-bucket idea for now: at
current follow/visit volumes, a week can have as few as 1-3 real follows,
too small for weekly aggregation to meaningfully reduce noise over daily
data. Moved from "outstanding" to "data_quality" as a recorded decision,
not silently dropped. Revisit if monthly volumes grow enough for the
technique to pay off.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added NON_CAMPAIGN_DATES (starting with 02/09), excluded the same way
REPOST_DATES already is — a real TXproducts post, but not part of the
24-post branding campaign, so it shouldn't skew "what worked" findings
that are specifically about the campaign's own content.
Also closed out July's "end-of-week boss report" outstanding item per
Mckinley: discarded, no longer tracked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was the long-standing unnamed "specific gap" flagged earlier — now
resolved and recorded in features_shipped, since it was never actually
written down as an outstanding item in the first place (tracked only in
conversation until now).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sourced directly from LinkedIn Branding Campaign Roadmap.pdf: August is
Stage 2, "Find the Audience" (Demographics); September is "Find the
Trigger," the end-of-campaign checkpoint. Core Message abstract factored
into a shared _CORE_MESSAGE_ABSTRACT constant and reused across all three
months rather than duplicated, since TXproducts' value proposition doesn't
change month to month the way the Roadmap stage does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hand-audited each against LinkedIn's own Reactions/Comments/Reposts lists,
same method as July and 04/08/06/08. Notable finds: Sabine Brock (DESY)
gave a substantive comment on 20/08, not just a like; Katrin Schmidt's
perfect July streak continued into August (20/08); Raphael Buck (CEO,
Get Inspired) is a new engaged non-follower, added to outreach_contacts.py.
25/08's audit entry is in place but inert until that post's data is
uploaded (export is only current through Aug 24).
Also cleaned up monthly_report_notes.py's July "outstanding" list: removed
a stale Real Eng %/Clicks methodology item that reads as still-open but was
actually decided and already implemented (see features_shipped).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every table, chart, and KPI that shows a per-post engagement rate now uses
Real Eng % (Audience-only, Team excluded, Clicks included, hand-audited) —
the live Content/Overview dashboard and every Reports page alike, not just
the Reports family. Audited posts show the real number; unaudited posts show
"Pending audit" in tables or are excluded (with a count) from charts, never
silently falling back to the raw, Team-inflated figure.
- New engagement_audit.py holds the audit data and Real Eng % calculation,
shared between app.py and report.py (previously app.py-only, which made
report.py's PDF unable to use it at all).
- compute_period_stats() (shared by the live app and the PDF) now computes
Real Eng % per post and an audited-only "Best Post Real Eng." max.
- Traction by Format's separate "Reported Avg Eng %" column is gone entirely
rather than shown side by side with the real one — one number, one meaning.
- Corrected an earlier internal claim from this same audit: 09/07 does not
drop to near-bottom under Real Eng %, it stays the clear #1 post (45.0%
real vs. 47.9% raw) since its large Click volume dominates the calculation.
Re-checked the Core Message "Emotional outperformed Rational" finding
against the real numbers rather than just relabeling it — it holds (21.8%
vs. 8.1% average, a wider gap than under raw numbers).
- Fixed two pre-existing bugs in build_static_export.py surfaced while
regenerating it: a stale 2-value unpack of _render_reports_tab's now
3-value return, and insights pages read from TAB_CONTENT_MAP instead of
the _INSIGHTS_PAGE_BUILDERS functions the dynamic-month refactor moved
them to.
- Documented the rollout, the 09/07 correction, and two remaining open
items (historical audit backfill scope, Avg. Eng. Rate's future
redefinition alongside Clicks/Impressions/CTR %) in Monthly Notes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dynamic-month architecture: all 7 Reports insights pages now read from a
shared reports-selected-month Store instead of being hardcoded to July, so
navigating between pages keeps whatever month is selected on Report
Overview. Hand-curated July-only content (Core Message trilogy/register,
Audience/Team audit) falls back to plain "not yet written up"/"pending
audit" messaging for other months instead of being faked.
- Audited 04/08 and 06/08 against LinkedIn's own Reactions/Comments/Reposts
lists, same method as July, extending _AUDIENCE_TEAM_AUDIT and Real Eng %
into August.
- Fixed Follower Study's composition stats to exclude internal team members
(was counting Daniel/Aleksandra/Peter/Mckinley as followers, inflating the
total past the real 111 shown elsewhere in the app).
- Added outreach_contacts.py: a running list of engaged non-followers
surfaced during audits, kept separate from the Follower Directory so it
can't reintroduce the same team-inflation bug.
- Roadmap's Traction by Format now shows both LinkedIn's raw Reported Avg
Eng % and the hand-audited Real Avg Eng %, with a link through to the All
Posts engagement breakdown; Engagement Over Time plots Real Eng % with a
pending-audit fallback.
- Documented the Real Eng % Clicks-inclusion methodology and recommendation
in July's Monthly Notes for the boss meeting.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restructures the Reports tab into a CSS Grid layout (Core Message + Campaign
Roadmap stacked, spanning Follower Growth below, alongside All Posts; Audience
Composition + Follower Study + Monthly Notes as a second row), each card
carrying a short abstract with a link to its full detail. Adds three new
"insights" sub-pages (Roadmap, Core Message, Monthly Notes) reachable via a
shared hero-banner nav so hopping between them doesn't require detouring
through the main Reports page each time. Also fixes two content-type
misclassifications (23/07, 30/07 should be Carousel) and trims chart chrome
(legend, axis labels/padding) to reclaim vertical space without shrinking bar
height.
PDF (report.py) and static export (build_static_export.py) intentionally not
yet updated to mirror these Reports tab changes — deferred until layout
iteration is done.
New "Reports" tab for the 3-month LinkedIn campaign, combining dashboard
data with the Follower Directory rather than a standalone HTML/PDF
workflow. For each campaign month it shows KPI tiles (including a new
Monthly Growth Rate, this month's net new followers as a percentage of
the followers already on the page at the start of that month, resets
every month against its own starting count), the follower growth chart,
every post published that month, an Audience Composition breakdown
sourced live from the Follower Directory (sector/seniority/country, both
this month's cohort and the all-time snapshot), and curated monthly
narrative notes (features shipped, data quality fixes, Directory
updates, outstanding items). Reuses the existing reportlab PDF pipeline,
extended with the same sections, and mirrored in the static exporter
with live-only controls disabled.
New files: monthly_report_notes.py (per-month curated narrative,
July populated), follower_directory_data.py (parses the Directory's
DATA array directly so numbers can't drift from the hand-maintained
source of truth).
Also fixes three pre-existing bugs found while building this: average
engagement rate was displaying 100x too small (a 0-1 fraction shown
as-is instead of scaled to a percentage), the Sector/Seniority/Country
breakdown tables were silently truncated below their own stated totals
(a hardcoded top-5 display cap plus a separate top-10 country cap
inside composition_stats itself), and the static exporter had no
support for dbc.Table, dropping the <table> tag entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>