70 Commits
Author SHA1 Message Date
beantownbabeandClaude Sonnet 5 cc827ca1e5 Move Follower Gains calendar/New Followers per Day directly under the KPI row
Reordered: calendar row now comes first in the content area (right under the
KPI tiles/Team member dropdown), followed by Visibility (full width), Posts
(full width), then the Impressions/Engagement Rate/Demographics row.

Refactored _build_person_dashboard/_build_empty_person_dashboard to return
their pieces (source_note, calendar_row, charts_row) separately instead of a
pre-assembled Div, so _render_team_tab controls the final ordering directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 08:47:59 +01:00
beantownbabeandClaude Sonnet 5 cfb5fd251f Give Visibility 25% width, Posts table 75% instead of an even split
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 08:44:32 +01:00
beantownbabeandClaude Sonnet 5 bd7406fe1e Top-align the Team member dropdown with the KPI row
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 08:41:26 +01:00
beantownbabeandClaude Sonnet 5 7baa757266 Vertically center the Team member dropdown with the KPI row
alignItems: flex-end was bottom-aligning the dropdown's column against the KPI
tiles, but the dropdown has a label above it eating vertical space the KPI
tiles don't have, so the dropdown control itself sat visibly lower than the
tiles. Switched to center so both align on the same visual middle line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 08:40:12 +01:00
beantownbabeandClaude Sonnet 5 79d15806ec Move KPI tiles next to the Team member dropdown; Visibility + Posts on one line
KPIs now render into their own area (team-kpi-area) placed directly next to the
Team member dropdown/Download Report row, instead of scrolled down inside the
main content area - extracted into _team_stat_row()/_team_empty_stat_row() and
wired as a third Output on _render_team_tab's callback.

Visibility and the Posts table now sit side by side in one row instead of each
being a separate full-width section. The Posts table (11 columns) gets its own
horizontal scroll container now that it's at half-width instead of full.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-31 08:32:39 +01:00
beantownbabeandClaude Sonnet 5 8f63b7dd53 Hide dormant years (2025) from the calendar year dropdown
Fixing the cumulative-total bug (previous commit) meant _team_fts_like now
spans the export's full true window, which includes trailing months from
before the person was active at all (Oct-Dec 2025, all zeros) - those were
showing up as a selectable year with nothing in it. _team_years_with_activity
keeps only years with at least one month of real net change or a real post;
2027 will appear automatically the first time it has either, same mechanism,
no code change needed then.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 14:33:40 +01:00
beantownbabeandClaude Sonnet 5 25d1cba05f Fix calendar cumulative total bug; make months clickable with a year dropdown
Bug: _team_fts_like truncated to >= TEAM_BASELINE_DATE (June 1) before computing
the cumulative running total, which silently dropped a real +1 gain from May 30
(one day before baseline). Two visible symptoms Mckinley caught directly: every
day's running total was permanently 1 lower than the real current follower
count (e.g. Oct 30 showed 20, not the real 21), and May disappeared from the
calendar entirely instead of showing its real "+1". Fixed by anchoring the
cumulative sum at the export's own true window start instead (confirmed
correct - summing every daily gain across the whole file exactly equals the
reported current total). TEAM_BASELINE_DATE remains intentionally separate,
used only for the Starting/New Followers KPI tiles (when this work started,
not literally the first follower ever).

Also made the calendar fully interactive: a year dropdown (ready for 2027) and
clickable month boxes, via one callback registered once and driven by pattern-
matching ids + State("team-member-select") rather than needing per-person
wiring. Guards against Dash's known quirk where a newly-mounted pattern-matched
button fires the callback once on its own with n_clicks still 0 - checks the
triggered input's actual value, not just which id fired, so a fresh render
doesn't spuriously jump to the wrong month.

Also dropped the Total Page Views / Total Clicks KPI tiles entirely (per
Mckinley) - neither exists in any personal-profile export, so a permanent "—"
placeholder for either was never going to show real data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 14:14:21 +01:00
beantownbabeandClaude Sonnet 5 0335e17e89 Add a 5-word Description column to the Posts table
LinkedIn's exports have no title/text field for a post anywhere (neither
Aggregate Analytics nor SinglePostAnalytics) - only the URL, whose slug is a
URL-safe truncation of the post's own opening words. post_title_from_url()
decodes it (handles accented names like Krutošíková), strips the trailing
-share-/-ugcPost-<id>-<code> suffix, and takes the first 5 words as a readable
stand-in for a title. Added to both the live dashboard's posts table and the
downloadable report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 13:08:33 +01:00
beantownbabeandClaude Sonnet 5 3de9143e46 Rebuild post_details_tracker.py as an auto-parser - no hand-logging needed
Turns out every individual post's own analytics page has an "Export" button
(SinglePostAnalytics_<Name>_<id>.xlsx) giving exactly the detail that was being
hand-logged: Profile Viewers/Followers Gained from the post, and the Reactions/
Comments/Reposts/Saves/Sends/Link/Premium-button breakdown behind the Aggregate
Analytics file's single blended "Engagements" number.

Matches each file to a post by its own "Post URL" field, not the numeric id in
the filename - confirmed those two ids are actually different LinkedIn schemes,
so filename-based matching would have been wrong. Loaded once per render
(load_post_details) rather than re-parsed per table row.

All 4 of Mckinley's real posts are now auto-covered (55/80/17/365 impressions),
replacing the 1-of-4 hand-logged coverage from before. Kept showing both
engagement numbers side by side in the table/report - the per-post export's own
breakdown sum has been observed to disagree with the aggregate export's
Engagements figure for the same post more than once now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 13:01:34 +01:00
beantownbabeandClaude Sonnet 5 60f75ba8b5 Team tab: real calendar (day tiles + post stars), one-line chart row, top-5 demographics
Calendar now matches the Overview tab for real: reuses _month_day_tiles to show
the default month's day-by-day breakdown underneath the month-box grid (not just
the month boxes alone), and the New Followers bar chart now overlays star markers
on publish days via the Page's own mark_posts(), using TEAM_BASELINE_DATE-filtered
data the same way the Page's own fts is filtered to >= BASELINE_DATE (so the
calendar/chart show the real June-October window, not the export's full noisy
~365-day span). Month-click-to-expand isn't interactive yet - would need its own
per-person callback wiring - so it always shows the most recent month.

Impressions, Daily Engagement Rate, Audience Demographics, and Content
Demographics now sit on one row (4 cards), and both demographics tables are
capped to their top 5 rows by Percentage instead of showing everything.

Logged two more real posts in post_details_tracker.py from screenshots (the
July 9 and Oct 22 posts) - only one post left unlogged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 12:54:57 +01:00
beantownbabeandClaude Sonnet 5 ea7aa06484 Add hand-logged per-post detail: Profile Viewers, Followers Gained, reaction breakdown
LinkedIn's export has no per-post Profile Viewers/Followers Gained attribution,
and its "Engagements" column is a single blended number - none of that exists
anywhere in the Aggregate Analytics file. Each individual post's own analytics
panel does show it, so this is a hand-logged, per-post-URL dict
(post_details_tracker.py, same add-an-entry pattern as the other two trackers).

Added as extra columns to the Posts table (both the live dashboard and the
downloadable report) alongside the exported Impressions/Engagements, rather
than replacing them - the two Engagement numbers have been observed to
genuinely disagree for the same post (export said 25, the post's own panel
summed reactions+comments+reposts+saves+sends to 20), so both are shown
instead of one silently overriding the other.

Seeded with one real logged post (the July 2 post: 3 profile viewers, 0
followers gained, 15 reactions, 5 comments).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 11:58:11 +01:00
beantownbabeandClaude Sonnet 5 833cfc6388 Add hand-logged Visibility row: Profile Appearances and Profile Views
LinkedIn has no file export for either metric (confirmed - no Download option
exists on the "Who's viewed your profile" / "Profile appearances" pages, unlike
the content analytics file team_exports.py parses), so this is a hand-maintained
log (visibility_tracker.py, same "add an entry, no code change needed" pattern
as conversation_tracker.py) rather than another auto-parsed file.

Shown as its own card above the main dashboard, independent of whether a
person's Aggregate Analytics export exists yet - visibility logging and the
Monday export are two separate data sources that can each arrive on their own
schedule. Seeded with Mckinley's real first week (Oct 20-26: 10 all
appearances +67%, 1 search appearance +0%).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 11:28:51 +01:00
beantownbabeandClaude Sonnet 5 0ed6e6c457 Team tab: June 2026 = 0 baseline, calendar grid + split charts to match Overview
Starting Followers is now a fixed, human-confirmed anchor (everyone started this
work June 1, 2026 with 0 followers) instead of borrowing the Page's own unrelated
BASELINE_DATE or the export's meaningless ~365-day window edge. Lives in
team_exports.TEAM_BASELINE_DATE/TEAM_BASELINE_FOLLOWERS so both the dashboard and
the downloadable report use the same anchor.

Rebuilt the chart section to mirror the Overview tab's actual layout: a static
Follower Gains calendar grid (reuses the Page's own _monthly_summary/_month_box_*
helpers, renamed columns to fit) + New Followers per Day bar chart side by side,
then separate Impressions and Daily Engagement Rate line cards instead of one
combined dual-axis chart. Daily CTR and Page Views (Desktop/Mobile) are left out
entirely - personal-profile exports have no click or device-level data at all,
so those two Overview cards have no real equivalent here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 11:18:43 +01:00
beantownbabeandClaude Sonnet 5 0c6baad4f1 Team tab: match Overview's exact 8-tile KPI row, fix baseline date, move report button to hero
Three corrections against the Overview tab's real KPI row:
- Tile set now matches exactly (Total Page Views / Total Clicks included as
  "not available for personal profiles" rather than silently substituting
  Members Reached in their place).
- Starting/New Followers now anchor to the same BASELINE_DATE (Jun 16, 2026)
  the Overview tab uses, not the export's own ~365-day rolling window edge -
  that was showing a meaningless "as of last October" date. Passed into
  build_person_report_docx() as a parameter (team_exports.py stays free of
  any app.py import, same rule report.py already follows).
- Download Report button moved from the tab's own row into the hero banner's
  upper-right actions slot, same spot the Followers tab's Directory/Study
  links live - the actual "upper right" of the page, not just of the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 11:06:37 +01:00
beantownbabeandClaude Sonnet 5 38101bde6f Team tab: real KPI row matching Overview's style, repost/original post markers
Replaces the ad-hoc stat cards with the same kpi() component the Overview tab
uses (Starting/Total/New Followers, Total Impressions, Members Reached, Best
Post Real Eng., Avg. Eng. Rate) - same visual language across the dashboard,
and "New Followers" in the window is the growth KPI that was missing.

The daily Impressions/Engagements chart now overlays star/diamond markers on
each post's publish day, split by Original vs Repost (mirroring the Page's
own mark_posts() star overlay) - the one thing this export doesn't give you
anywhere else as a labeled field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 11:00:28 +01:00
beantownbabeandClaude Sonnet 5 514e21952d Give every team member the same box layout, even with no data yet
Peter, Daniel, Niklas, and Aleksandra were showing a single plain text
alert while Mckinley got the full stat-tile/chart/table layout - now
everyone gets the same boxes (stat tiles, chart cards, posts table,
demographics cards), just with "No data yet" placeholders where a
person hasn't submitted an export. Keeps the tab visually consistent
across all five regardless of who has data yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 10:54:23 +01:00
beantownbabeandClaude Sonnet 5 db4587f75e Build real Team tab parsing from LinkedIn's personal Aggregate Analytics export
Replaces the empty placeholder with actual parsing of the "AggregateAnalytics_
<Name>_<start>_<end>.xlsx" personal-profile export (DISCOVERY, ENGAGEMENT, TOP
POSTS, FOLLOWERS, and both DEMOGRAPHICS sheets). Weekly submissions are a new
~365-day trailing window each time, so the daily series (engagement, followers)
merge across every file on disk the same way the Page's own Followers export
does - newest file wins on an overlapping date.

Reposts vs original posts are distinguished from the post URL itself
(.../share-<id> vs .../ugcPost-<id>), since LinkedIn's "Engagements" column
doesn't separate them - this was the open design question about measuring
repost/comment activity, and the data turns out to answer it directly.

Verified against Mckinley's own first real export (2026-10-30): Impressions,
Followers, and Engagements all cross-check against the file's own DISCOVERY
summary row. The other four team members still show the "no export yet"
placeholder until their first Monday submission lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 10:23:59 +01:00
beantownbabeandClaude Sonnet 5 436e52254a Add Team tab with per-member submenu (data starts Dec 2026)
New tab for analyzing each team member's own personal LinkedIn export
data (separate from the Page-level Content/Followers/Visitors exports),
starting December 2026 per Mckinley. Submenu dropdown lists Peter Gaal,
Daniel Schmidt, Niklas Meidow, Aleksandra Kotenko, Mckinley Black.

Same "starts empty" pattern as the Engagement Response tab: a hand-
maintained team_exports.py module (empty for now) backs the tab, and
each person shows a clean placeholder until their real export data
gets added - no other code changes needed at that point.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-30 10:10:04 +01:00
beantownbabe ad2f8698f7 Drop "TXproducts —" prefix from CTA Campaign phase headers
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.
2026-10-26 18:06:47 +01:00
beantownbabe 91b9f3f232 Drop "End of Month Campaign Report" suffix from CTA Campaign headers
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.
2026-10-26 14:19:25 +01:00
beantownbabe 03e6d7287e 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.
2026-10-26 12:22:40 +01:00
beantownbabeandClaude Sonnet 5 3a7a47e3f9 Drop redundant "CTA Campaign" prefix from the phase title
The Campaign dropdown already says "CTA Campaign" right next to it -
repeating it in the header buried the thing that actually matters,
the timeframe. Title now leads with the date range itself (e.g.
"Nov 3 - Nov 26 (Failure Analysis)").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-26 12:11:53 +01:00
beantownbabeandClaude Sonnet 5 f54db563a3 Two-level Reports nav: Campaign dropdown, then Period dropdown
Replaces the single flat "Reporting Month" dropdown with two linked
ones - Campaign (LinkedIn Branding Campaign / Pilot Partner / CTA
Campaign), then Period, populated based on the campaign picked.
Dash's dropdown has no native nested submenu, so this is the standard
"pick a year, then a month" cascade instead of faking nesting in one
control.

LinkedIn Branding Campaign -> July/August/September/End of Campaign
(unchanged, same data as before). Pilot Partner -> October (unchanged).
CTA Campaign -> its 3 proposed phases by date (Nov 3-26, Dec 1-Jan 7,
Jan 12-Feb 4), each showing a "not approved yet" placeholder rather
than crashing - compute_period_stats()'s date parsing doesn't
understand "cta-phase-1" as a period key, so it's guarded the same way
"campaign-end" was guarded before that report was built out. Download
button is guarded the same way.

Dropdown structure only - no CTA Campaign report content built yet,
that's still pending approval.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-26 11:51:31 +01:00
beantownbabeandClaude Sonnet 5 efcad4a6da Add Engagement Response tab for the new campaign (pending approval)
New tab, separate from the Reports dropdown (which stays tied to
actual calendar-month/campaign-approval data), covering what the new
campaign plan cares about that the old reporting never tracked:

- theme_tracker.py: per-post theme classification (Failure Analysis /
  Standardization / WaveGate), same hand-tagged pattern as
  MANUAL_CONTENT_TYPES. Powers a Theme Performance chart showing each
  theme's Real Eng % across the whole campaign, not just its lead
  month - the explicit ask was to see if a theme keeps earning
  engagement even when a different theme is leading that month.

- conversation_tracker.py: logs every real conversation (a comment
  from an actual person) through to resolution (open -> replied ->
  followed_up -> closed). Exists because the old campaign's two real
  conversations, Sabine Brock on 20/08 and Dr. Debnath on 08/09, were
  both left open with no reply and no follow-up - this makes that
  visible instead of silent. An "N open" badge flags anything still
  needing a reply.

Both data sources start empty and render a clean "nothing logged yet"
state - nothing to log until the campaign is approved and posts start
going up, but the tooling is ready.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-26 11:41:21 +01:00
beantownbabeandClaude Sonnet 5 b71a7d15e2 Plain title for post-campaign months like October
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>
2026-10-20 12:11:30 +02:00
beantownbabeandClaude Sonnet 5 268c44cc62 Remove End of Campaign from the Reports month dropdown
Already delivered as its own document, no need for it to stay
selectable in the live dashboard. The underlying generation code
(build_report_docx's campaign-end support) is left in place in case
that document needs regenerating later, just not exposed in the UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-20 12:00:39 +02:00
beantownbabeandClaude Sonnet 5 cd059076e0 Add October 2026 to the Reports month dropdown
The 3-month campaign (July-September) wrapped, but posts have kept
going (01/10 already up). October wasn't selectable in the Reports
tab at all - added it to _REPORTS_PERIODS. Falls back to "Executive
Summary not written up yet" until October's narrative is written,
same graceful handling already in place for any unwritten month.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-20 11:49:19 +02:00
beantownbabeandClaude Sonnet 5 514fd4a279 Add "Greater Boston" to location-country overrides
LinkedIn omits the country for this metro area too, same pattern as
San Francisco Bay Area / Greater Madison Area / Los Angeles
Metropolitan Area — was dropping it from the followers map with a
console warning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-05 08:42:52 +02:00
beantownbabeandClaude Sonnet 5 32cea004ed Pale yellow headers on campaign-end composition tables
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>
2026-10-02 15:33:41 +02:00
beantownbabeandClaude Sonnet 5 c5b6483b88 Add campaign-wide Audience Composition table to the campaign-end report
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>
2026-10-02 15:24:38 +02:00
beantownbabeandClaude Sonnet 5 46b5da00ce Add explicit Insights and Opportunities sections to campaign-end report
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 15:20:28 +02:00
beantownbabeandClaude Sonnet 5 487e1df796 Fix 13/08's Content Type: Carousel, not Text/Image
LinkedIn's export doesn't distinguish Carousel/document posts from
generic Image, so MANUAL_CONTENT_TYPES already exists to hand-correct
known misclassifications - 13/08 was simply missing from it. Mckinley
confirmed directly: all top-3 campaign posts by Real Eng % (04/08,
13/08, 09/07) are Carousels, not a mix of Carousel and Text/Image.

Dashboard only per Mckinley's explicit instruction - the July/August/
September Word reports are already submitted and intentionally left
unchanged. Static export regenerated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 14:57:03 +02:00
beantownbabeandClaude Sonnet 5 5bf0b6fdd8 Add manual title for 29/09 post: "The two pillars of TXservices"
LinkedIn's export had no caption text for this image/video-only post.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 11:14:20 +02:00
beantownbabeandClaude Sonnet 5 d09037945f Correct 29/09 post's Content Type: Carousel, not default Text/Image
LinkedIn's export doesn't distinguish carousel/document posts from
generic Image, so this needed the usual manual override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 11:13:49 +02:00
beantownbabeandClaude Sonnet 5 6722b29fe8 Build the End of Campaign report (3-month synthesis + Oct recommendations)
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>
2026-10-01 10:49:47 +02:00
beantownbabeandClaude Sonnet 5 9cbfe83f41 Label Avg. Eng. Rate as raw/unaudited everywhere, resolve the open item
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>
2026-09-30 17:47:10 +02:00
beantownbabeandClaude Sonnet 5 292bd21abe Generate EOM reports as editable .docx instead of fixed-layout PDF
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>
2026-09-30 14:19:29 +02:00
beantownbabeandClaude Sonnet 5 f254a9f10f Strike through excluded posts in Reports tab's mini All Posts card
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>
2026-09-30 12:50:48 +02:00
beantownbabeandClaude Sonnet 5 9eb636bf79 Fix misleading "Pending audit" label on excluded posts in Overview
The Overview/Content tab's Posts table labeled every unaudited Real
Eng % as "Pending audit", including the 10/07 repost and other
REPOST_DATES/NON_CAMPAIGN_DATES posts that are deliberately never
audited (see engagement_audit.py) rather than simply not gotten to
yet. That read as an incomplete job rather than an intentional
exclusion. Those dates now show "Excluded" instead, matching the
wording already used in the Reports tab's All Posts and Core Message
treatment of the same dates. Fixed in both the initial table render
and the month-dropdown callback path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-30 12:05:04 +02:00
beantownbabeandClaude Sonnet 5 1718a56ea6 Add Total Followers (month end) tile to Reports tab and PDF
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>
2026-09-30 11:48:24 +02:00
beantownbabeandClaude Sonnet 5 5eafbd972b Remove Total Followers tile from Reports tab and PDF
"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>
2026-09-30 08:26:59 +02:00
beantownbabeandClaude Sonnet 5 e2343044d4 Add manual title override for 03/09 (was showing as blank)
LinkedIn's export has no caption text for this post, so it fell back to
"(no text — image/video-only post)" instead of its actual content, "Come
meet us at SNIB2026" — looked like a missing/unaudited post even though
it was already fully audited underneath.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:31:18 +02:00
beantownbabeandClaude Sonnet 5 9859b379c9 Show the repost as an excluded row in All Posts, not silently absent
10/07 was fully filtered out of the Engagement Composition table by
_reports_posts_f() (correct for calculations, but invisible to a reader).
Now it appears struck through with a pale-red background and "Excluded"
written next to it, same transparency treatment Core Message's register
table already gives it — visible and clearly marked, not hidden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 08:59:21 +02:00
beantownbabeandClaude Sonnet 5 3ea96e1521 Exclude the two pre-campaign June posts from campaign analytics
The branding campaign only runs July-September; 19/06 and 24/06 predate it
entirely, same treatment as 02/09. Added to NON_CAMPAIGN_DATES.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 08:52:32 +02:00
beantownbabeandClaude Sonnet 5 f08fb1d1c4 Exclude non-campaign posts from format/day/traction breakdowns
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>
2026-09-14 08:37:24 +02:00
beantownbabeandClaude Sonnet 5 ce773717d5 Add Los Angeles Metropolitan Area to location-country overrides
Fixes "no centroid for X" console warning surfaced on the Aug 31 export
that closes out August (a new US-metro follower without a country suffix
in their location string, same pattern as San Francisco Bay Area and
Greater Madison Area).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 17:09:18 +02:00
beantownbabeandClaude Sonnet 5 9155e708b4 Make Follower Study's cumulative tables tell the monthly story, not just repeat it
Per feedback: a monthly report showing the same near-identical cumulative
tables every month, marginally bigger each time, isn't earning its keep.
Fixed by making the cumulative views themselves show where this month's
people landed, instead of adding that only as a separate summary:

- Account Clusters now annotates each company's growth inline (e.g. "DESY:
  5 (+3 this month)") instead of a flat all-time count with no monthly
  story.
- Notable Contacts highlights rows added this month in place, in context
  with everyone else at that seniority level, instead of only being named
  in a separate bullet list above.
- Dropped the cumulative Sector/Function/Seniority/Country table entirely —
  Audience Composition already owns that exact breakdown for the month,
  so repeating the all-time version here was pure duplication with no
  monthly-specific value.
- Kept the Sector x Seniority crosstab as pure cumulative reference (still
  needs the full ~100+ scale to mean anything, no individual-level "new"
  story to tell).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 13:51:24 +02:00
beantownbabeandClaude Sonnet 5 5e20823947 Add "what changed this month" to Follower Study, ahead of the cumulative picture
Feedback: showing "102 followers to date" as the headline of a monthly
report answers the wrong question — a reader wants to know what happened
in the month the report covers, with the cumulative numbers as supporting
context, not the other way around. Audience Composition already did this
correctly (month-scoped "22 new followers" by design); Follower Study now
leads the same way: which new Notable Contacts joined this specific month,
and which Account Clusters grew, computed live for whatever month is
selected. The existing cumulative "bimodal seniority"/"shallow-network"
findings stay, just demoted to a clearly-labeled "full picture, to date"
section beneath.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 10:48:50 +02:00
beantownbabeandClaude Sonnet 5 d4fff48384 Add real findings to Audience Composition and Follower Study cards
Both cards were just raw pivot-table dumps (Sector/Function/Seniority/
Country counts) with no interpretation — a reader had to notice everything
themselves. Added a "What This Month's/These Followers Tell Us" card to
each, computed fresh from whatever month is selected rather than hardcoded
prose, so it stays accurate for any month rather than describing one
snapshot forever:

- Audience Composition: dominant sector this month, country concentration,
  and any notable (C-Level/Director) new followers gained that specific
  month.
- Follower Study: a live version of the same "bimodal seniority" and
  "shallow-network audience" findings the hand-authored follower_study.html
  already makes for July, but computed from the actual Directory data
  through whatever month is selected instead of frozen to one snapshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 11:39:05 +02:00
beantownbabeandClaude Sonnet 5 b9365a2cea Spell out Real Eng % and CTR % formulas explicitly in every callout
Feedback: the formula was never actually written out anywhere in the app,
just described in prose (what's included/excluded), so it was genuinely
hard to find even after having built the whole audit system. Added a
shared, explicit formula + worked example (fake round numbers, never a
real post's) to all four places these metrics have their own callout:
Engagement Composition, Traction by Format, Real Engagement Over Time, and
two new ones — Best Posting Day and Report Overview's posts list — which
previously had no explanation for either metric at all.

Also added the "measures attention, not website traffic" correction to
every CTR % explanation: TXproducts' links always live in a comment, never
the post body, so LinkedIn's per-post Clicks metric can't be counting
outbound website visits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 13:37:29 +02:00