Header is now just the date range and lead theme (e.g. "Nov 3 - Nov
26 (Failure Analysis)"), no company-name prefix — the Campaign
dropdown above it already names both the campaign and the company.
CTA phases now get the same plain "TXproducts — {timeframe}" header
every non-branding-campaign period already uses, instead of the
branding campaign's own "...End of Month Campaign Report" phrasing —
the Campaign dropdown above it already names the campaign, no need to
repeat it in the header too.
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.
Only July/August/September were part of the 3-month branding campaign
(CAMPAIGN_START-CAMPAIGN_END). A later month has no active campaign
stage behind it (no goal_line, no core_message/roadmap abstract), so
calling its report "...End of Month Campaign Report" would be wrong.
October (and any future month) now gets a plain "TXproducts — {Month
Year}" title instead, in both the live Reports tab and the docx.
July/August/September's titles are unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the pale yellow (#fdf6d8) already used for analytical table
headers elsewhere in the app. Added an optional header_bg param to
_composition_table (live preview) and passed the same color to
_docx_table (docx) for the Sector/Function/Seniority/Country tables
specifically, leaving the one other _composition_table call site
(Reports tab's compact card) untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mckinley manually added Sector/Function/Seniority/Country breakdown
tables to each monthly report by hand from the live Audience
Composition page; the campaign-end report had no equivalent since
there's no single month's page to pull a 3-month combined table from.
Generated directly in both the docx (report.build_report_docx) and the
live Reports tab preview (app._render_campaign_end_summary): merges
Follower Directory rows whose followedSort falls in {2026-07, 2026-08,
2026-09} (58 new followers profiled across the campaign) and renders
the same 4-table breakdown as the monthly reports use.
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>
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>
A summary page that leaves out the actual numbers isn't a summary —
moved the Key Metrics table (and its Monthly Growth Rate note) up
onto the same page as the Executive Summary's goal/verdict/pros/cons,
ahead of the page break. A reader who only reads page 1 now gets
both the verdict and the real numbers behind it; everything else
(Core Message, Roadmap, Follower Growth, All Posts, deep dives) stays
as the optional detail past the break for whoever wants more than
one page.
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>
Mckinley's PDFs kept cutting off content mid-page with no way to fix
it short of regenerating the whole report — a fixed-layout PDF isn't
the right format for something meant to be edited. Adds
report.build_report_docx(), a python-docx equivalent of
build_report_pdf() sharing the exact same compute_period_stats() data
and section order (KPI table, section abstracts, weekly pace,
audience composition, follower growth chart, all posts table with
the same excluded-post strikethrough treatment, format performance,
competitor snapshot, and the full Section Deep Dives for a monthly
campaign report), so the two can never silently drift apart on
numbers even though the rendering layer is completely different.
Word reflows content automatically, so there's no more fixed page
grid to fight when editing.
The Reports tab's download button now produces .docx instead of PDF.
Verified via python-docx re-read (KPI values, the 10/07 repost row's
strikethrough, embedded chart images) and strict OOXML schema
validation (including fixing a `w:zoom` attribute python-docx's own
default template omits). build_report_pdf() is left in place, only
report.py's ctx docstring rewritten since the doubly-stale "sidebar's
generic PDF control" no longer exists as an actual call site.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Reports > July landing page's own "All Posts, July 2026" card
(_reports_posts_table, fed by report.compute_period_stats()'s simple
date-range filter, not app.py's REPOST_DATES/NON_CAMPAIGN_DATES-
excluding _reports_posts_f()) showed the 10/07 repost as a plain,
unmarked row with "Pending audit" — inconsistent with the dedicated
All Posts insights page one click away (struck through, "Excluded")
and confusing on its own, since "Pending audit" reads as an
unfinished job rather than a deliberate, permanent exclusion.
Fixed in both places this table renders: the live Reports tab
(app.py's _reports_posts_table) and its PDF equivalent (report.py's
"All Posts" table), both now applying the same pale-red/strikethrough
"Excluded" treatment already used by the register table and the
dedicated All Posts insights page. Added non_campaign_dates to
_build_report_ctx() so the PDF can apply the same exclusion set, not
just repost_dates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Month Start and New Followers already existed, but neither answers
"how many followers do we actually have as of this month" without
the reader doing the addition themselves. Adds a third tile, Total
Followers (month end) = Month Start + New Followers, placed between
them so the row reads Start -> New -> End as one story. Pinned to
that specific reporting period like Month Start already is, so it
never drifts with "today" the way the tile removed earlier did.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Total Followers (as of today)" was confusing on a monthly report
read days or weeks after the month closed, since the live/global
follower count keeps drifting past whatever month is being reported
on. Month Start already gives the one followers figure that's
actually pinned to that specific reporting period, so the redundant
tile is dropped entirely rather than repurposed. Live dashboard tabs
outside Reports are unaffected and keep showing the live total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CTR % used to sit after Real Eng %, splitting it from Impressions/Clicks
(the two numbers it's actually derived from) in every per-post table:
Report Overview, Best Posting Day (live + PDF), and the Content tab. Now
every one of these reads traffic numbers first (Impressions, Clicks, CTR %),
then the headline Real Eng % takeaway, then raw reaction counts where
present.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Best Posting Day (live app and PDF) was grouped by weekday instead of date,
making it confusing to line up against All Posts, Report Overview, and Core
Message's register, which are all chronological. The day/format confound it
was grouped to highlight is already called out explicitly by the red-flag
banner below it, so the grouping added confusion without adding information.
Also flipped the Content tab's global posts list from newest-first to
oldest-first, so a given post sits in the same relative order in every table
in the app, not just within the Reports/PDF family.
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>
- Roadmap Insights: stage-sequence timeline, per-format traction table with posts named
inline, best-posting-day calendar with weekday/post detail table flagging the
format/day confound, engagement-over-time chart (icons throughout, yellow banner
titles matching Core Message)
- Core Message Insights: icon set for the four-step framework and each card
- Reposts excluded from every format/day/traction breakdown (REPOST_DATES) across
app.py, report.py and the static export
- Follower Growth Insights and All Posts Insights now mirror the Overview/Content/
Growth tabs' own charts, scoped to July only, with matching KPI rows
- Monthly Notes moved out of the Reports tab into its own sidebar link (personal
accounting, not part of the campaign report)
- Audience Composition and Follower Study insights pages expanded: full Sector/
Function/Seniority/Country breakdowns, a Sector x Seniority cross-tab, and account
clusters (2+ followers from the same company) for the whole pre-August Directory
- report.py and build_static_export.py mirrored throughout to stay in sync with app.py
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a "This Month's Sections" abstracts block after Key Metrics (Core
Message, Roadmap, Follower Study, Monthly Notes), trims the existing
Audience Composition table to Sector-only to match the live card, and
appends a new "Section Deep Dives" block covering all 7 sections in Reports-
tab order: Core Message's full Analyze/Extract/Formulate/Validate cycle
(including the July register table with the repost highlighted and the
trilogy finding), Roadmap's Best Posting Day, All Posts' Traction by Format
and Top Posts, Follower Growth's weekly breakdown and growth windows,
Audience Composition's Seniority/Country, Follower Study's Notable Contacts,
and Monthly Notes' full narrative (moved here from its old position earlier
in the report).
Also fixes two real rendering bugs surfaced while testing: reportlab's base
Helvetica font can't render certain accented characters (e.g. Milena
Petkovic's c-acute showed as a black box) — added an ASCII-fallback helper —
and two long company names were overflowing into the Country column, fixed
by wrapping those cells in Paragraphs instead of plain strings.
Verified: both the monthly (July, 7 pages) and generic "All Time" (2 pages,
unaffected) PDF paths generate cleanly, and the live Download PDF button
produces the same 7-page output as the direct build_report_pdf() call.
build_static_export.py still does not reflect the live Reports tab — next up.
Reports tab polish:
- Title renamed to "{Month} End of Month Campaign Report" (TXproducts dropped,
the sidebar already identifies the tool), moved inline between the month
dropdown and Download PDF button, sized to fit on one line.
- All 9 KPI tiles restyled: centered, uniform size, orange accent (distinct
from the app's navy default), drop shadow. Total Followers/Month Start/New
Followers show as the headline numbers. Growth Rate's explanation moved
from a permanent caption into a yellow (?) popover, consistent with the
Audience Composition coverage note (also moved into its own header badge).
- Follower Growth and All Posts shown side by side, equal height, All Posts
widened so no title wraps and shows every post with no scrolling.
- New "Month Start" KPI: cumulative followers at the start of the period,
the denominator the new Monthly Growth Rate is computed against.
- Corrected the Follower Directory coverage note: it's updated by hand each
time a new follower comes in, so it can run ahead of the dashboard's own
weekly LinkedIn export, not the other way around.
Multi-month support:
- Reports dropdown now has fixed slots for the whole 3-month campaign
(July/August/September) plus a distinct "End of Campaign" entry, rather
than only showing months with notes already written. August/September
render with the same template as July, gracefully empty until data/notes
exist. "End of Campaign" shows its own placeholder instead of running the
normal per-month stats path, it gets a different design once the campaign
wraps, not just another empty month.
- Fixed _period_label to build "Month Year" directly from the timeframe
value instead of looking it up in export data, which failed for a future
month with no export rows yet.
Removed the sidebar's Upload New Data and Generate PDF Report controls
(_handle_upload, _generate_report, the dcc.Upload/report-timeframe-select
components, and the now-unused report_month_options() helper). Neither was
part of the real workflow: data refresh happens by copying files in and
restarting the process, and the sidebar's generic PDF button is fully
superseded by the Reports tab's own per-month PDF download.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Dash app with Overview, Content, Followers, Visitors, Competitors, and
Growth tabs. Includes weekly-pace tracking, PDF report generation, data
upload with live refresh, and a static HTML export builder for sharing
via NextCloud.