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>
Auditing our own audit data turned up a real error: Mihail Petev was
recorded as an audience repost of 04/08, but he actually liked the
separate company-page reshare surface (already excluded from the Real
Eng % audit for a different reason — mismatched Impressions denominator)
— not a repost of the original post. The two real reposts of 04/08 were
Peter Gaal and Mckinley Black, both Team. Reposts: audience 1→0, team
stays 2. Real Eng % for 04/08 moves from 49.8% to 49.4% accordingly.
Directory note corrected to match.
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>
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>
follower_directory.html's addition landed in 070710f incidentally (shared
working directory with the End of Month Report session); this completes
the required Study recompute: seniority/function Unknown +1, no-company
pool 16->17, India promoted to its own Geography row (1->2), 3rd-degree +1,
growth H2 2026 +1. All six charts and every affected percentage verified.
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>
- Sabine Brock (DESY, dep. CTO, 10th DESY-linked entry), Daniel
Hernandez Duran (TUHH/DASHH PhD candidate), Yash Bhatia (Hannover
optical-technologies research assistant)
- Full recompute of all six Study charts and every affected percentage
- Notable Clusters table: DESY row 9->10, TUHH row 2->3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Kirsty Paton (Synchrotron SOLEIL), Dr. Palvan Seyidov (Nexperia), Vishnu
Narayan Manoj Kumar (DESY, 9th DESY-linked entry), Bridget Murphy (DESY/
University of Kiel Group Leader), Maximilian F. (Inspiralia)
- Full recompute of all six Study charts and every affected percentage/count
each time (Seniority, Function, Sector, Geography, Connection, Growth)
- Fixed a follower_directory.html/study.html sync gap: the second Mckinley
Black entry (Data Governance Consultant) was missing Company/Sector/
Country/City, silently excluded from TXproducts-internal counts
- Added blank MANUAL_POST_TITLES entry for the 2026-08-13 post
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reordered the first line to state plainly up front that Real Eng % is
hand-audited before explaining the Audience/Team split mechanism, per
feedback on wording.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New "Most Reach" spotlight: a medallion badge (layered rim + gradient disc,
not a literal trophy/ribbon icon) showing which audited post had the highest
combined Audience Acknowledgment+Conversation+Amplification this month —
sitting to the right of the table behind a divider, showing only the format
icon and date so it points at the winning row rather than repeating numbers
that already live in the table.
Renamed the Read column's per-repost "Reach" label to "Amplification" —
it collided with the new medallion's broader "Most Reach" (which sums all
three categories), so the same word meant two different things on the same
page.
Also condensed the Engagement Composition callout from three dense
paragraphs down to three short bolded-lead-in lines, per feedback that it
was too wordy to scan quickly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The stacked Likes%/Comments%/Reposts% bar chart was the last thing on this
card still showing raw, unaudited, blended Audience+Team data, inconsistent
with everything else on the page. It was also increasingly redundant once
Impressions/Clicks/CTR% were added to the table itself. Dropping it fixes
the cramped layout the new columns caused, no data lost — the table already
shows this same information audited and per-post.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real Eng % = (Audience Likes+Comments+Reposts+Clicks) / Impressions, but two
of those four ingredients (Impressions, Clicks) never appeared in the table
where the percentage itself lives — you had to go to a different table to
see what a post's Real Eng % was even calculated from. Added Impressions,
Clicks, and CTR % as plain columns (grouped before Real Eng %, matching the
ordering already used elsewhere); they never get the Audience/Team diagonal
split since LinkedIn never attributes an impression or click to a specific
person the way it does a Like/Comment/Repost.
Also fixed the unaudited-row fallback for Likes/Comments/Reposts: it was
showing a share-of-total percentage (e.g. "52%"), the exact small-denominator
distortion this whole audit exists to avoid. Now shows the plain raw count,
consistent with every other "not yet audited" fallback in the app.
_audience_team_td (the diagonal box renderer) moved to module level so it
can be reused when this same table design gets migrated to Best Posting Day
and the Content tab next.
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>
- 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>
- 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>
Extends build_static_export.py's render() with three things it never needed
before this session's Reports tab work: dcc.Link support (internal routes
become switchTab() calls, since a single static file has no server-side
routing; asset-relative hrefs stay real links), html.A support (href/target
were previously dropped entirely, falling through to the "render children
only" fallback), and Ul/Li in the tag map (bullet lists were silently
degrading to plain unstyled <div>s, a gap already known from the CSS
Ul/Li was never added). Also renders the 7 new insights pages (not in
NAV_ITEMS, reached only via the new hero nav) and adds per-tab hero-action
rendering to switchTab(), which fixes a pre-existing gap where the Followers
tab's Directory/Study links were computed but never actually shown, since
hero-actions was previously baked once for "overview" and never refreshed on
tab switch.
Also fixes a real bug this surfaced: grid-template-areas values used double-
quoted strings ('"cm rm ap" "fg fg ap"'), which silently terminated the HTML
style="..." attribute they were concatenated into (itself double-quoted),
corrupting the entire Reports tab grid layout in the static file only (the
live app was unaffected, since React sets styles via the DOM API, not string
concatenation). Fixed at the source (single-quoted CSS strings) and hardened
style_dict_to_css() itself to escape embedded quotes going forward.
Verified: all 7 insights pages render and cross-navigate correctly in the
static file, the CSS grid layout now matches the live app exactly, and a
full smoke test of the pre-existing tabs (Overview through Growth) shows no
regressions.
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.
Gives each of the 7 Reports sections (Core Message, Roadmap, All Posts,
Follower Growth, Audience Composition, Follower Study, Monthly Notes) its own
dedicated page instead of 4 shared ones, with a consistent cross-page hero nav
(plus a Report Overview link back) so none of them dead-end.
Core Message Insights is rebuilt around the actual Core Message document's own
4-step revision cycle (Analyze/Extract/Formulate/Validate, p.2-8), each step
backed by live data or, where LinkedIn data can't answer it (Validate's real
audience interviews), honestly marked as not yet done rather than backfilled
with a proxy metric. Formulate includes a hand-built Rational/Emotional gauge
and a post-by-post register table verified against actual post content (not
just export captions) — surfaced a deliberate 3-post narrative arc (14/07 to
28/07 to 30/07) that delivers the Core Message's own value proposition through
a personal-voice format, not the captions-only read that started this pass.
Follower Study is now a native page (live "Notable Contacts" pulled from the
Directory) instead of a link to the external hand-authored document, so every
nav item behaves the same way.
report.py and build_static_export.py still do not reflect any of this — next
up.
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.
The Board and Handbook callouts named Mckinley Black specifically as
the one connecting; Peter Gaal and Daniel Schmidt are also TXproducts
admins, so wording now refers to "whichever admin" is doing the
outreach. Also swapped both callouts from the blue gradient to the
existing pale-yellow controls-bg treatment, with light/dark variants.
goto_messaging.html no longer holds cut-and-paste message templates,
it's now a pure explanation of LinkedIn's Page vs. personal-profile
mechanics, connection-degree rules, and what actually grows the
TXproducts follower count (personal invites, Premium Company Page,
and Sponsored Content/Campaign Manager), with links back to the
Directory and Outreach Board.
follower_directory.html: renamed the third toggle button to "Handbook
of the Rules", added #board hash support so links can open straight
to the board, and added a callout on the board clarifying it drives
personal relationships/engagement, not the Page follower count.
Adds assets/goto_messaging.html: a modular intro/tenure/sector/ask
message matrix for LinkedIn follower outreach, reachable from the
Follower Directory's view toggle row.
Sinan Bubik (ITU/Istanbul Technical University, Partner - Business
Development, personally in Munich) added to the Directory and Study,
full recompute applied. Turkey is a brand-new country in this dataset.
Checked against the Jul 28 vs Aug 3 export diff and confirmed not yet
visible in any export, dated August 2026 same as recent additions.
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>
Reviewed LinkedIn's own followers list, ordered by actual follow date,
which directly confirmed her real join month as July 2026, not August
as previously assumed from indirect Location-export diffing. No
headcount change (110 to 110), only date attribution.
This resolves the "one more real follower still unidentified" gap
flagged in the July 31/Aug 3 correction: the Directory's July cohort
now matches the dashboard's own LinkedIn-sourced net new followers
figure for July exactly (22 to 22).
Peter Gaal, an existing follower dated March 2023, was also flagged by
the same followers list as showing July 2026, but that correction is
not applied yet: moving him too would push the July cohort to 23
against the dashboard's confirmed 22, so it needs further review
before changing.
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>
Alexander Rack (ESRF synchrotron scientist, Grenoble) and Tina Schmidt
(HZDR innovation manager) added to the Directory and Study, full
recompute applied. Both checked against the Jul 28 vs Aug 3 export
diff and confirmed genuinely not yet visible in any export (unlike
the last 3 additions), so dated August 2026 same as before.
Milena Petković, Paul Evans, and Simone Vadilonga were dated August
2026 on assumption (their profile-page screenshots showed no explicit
follow date). Diffing the Location breakdown between a July 28 and
August 3 export showed exact matches for all three (Greater Madison
Area appearing fresh, Berlin increasing by exactly 2), placing their
real join dates in the already-tracked July 30-31 growth window instead.
Corrected to July 2026 in the Directory — no headcount change (still
108), and no change to the Study's growth timeline since both months
fall in the same "2026 H2" bucket. Added a methodology note explaining
the correction and flagging one more July 30-31 follower still unidentified.
These are hand-maintained data, not regenerable from code, so unlike
the dashboard snapshot they need a real backup rather than a
regenerate-on-demand exclusion.
Follow Rate Relative to Reach was defaulting to the newest month even
when it had only a single day of data, making the chart look broken on
load — now defaults to whichever month has the most data instead.
Also add "Greater Madison Area" to the location-country override list;
this week's export introduced a new US follower location with no
country suffix, same pattern as San Francisco Bay Area.
- Hero banner now shows Follower Directory / Follower Study links on the
Followers tab only (not the sidebar), opening the standalone reports served
from assets/.
- Followers by Country: _COUNTRY_CENTROIDS expanded from 14 hardcoded entries
to a near-complete world table, so a country missing from the old dict no
longer vanishes from the map silently; added a console warning for any
country still missing a centroid, and a "N countries represented" caption
computed from the same rollup the map uses so the two can never disagree.
- Added visitor_history.py: append-only observation log for the Visitors
export's category breakdowns (Location/Seniority/Company size), since
LinkedIn only shows a category if it clears a per-export minimum-visits
threshold and otherwise silently drops it.
- Synced other accumulated app.py/build_static_export.py work not yet
committed: calendar month drill-down tiles, follower-breakdown expand
toggle, CTR explainer, manual post title overrides, and matching static-
export precompute/JS for the calendar and content tabs.
- .gitignore: exclude the newer generated snapshot outputs (follower study/
directory HTML copies, PDF report, visitor_category_history.json) using
the same rationale as the existing growth_history.json/snapshot rules —
regenerable from code + data, not itself code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- All Posts table now scrolls within a fixed height instead of paginating/
growing indefinitely as more posts accumulate, with column widths tuned
so Short title gets the most room and narrow columns (dates, rates,
counts) stay compact.
- _find_latest_file/_find_all_files now only match real LinkedIn export
filenames (txproducts_*), so unrelated files sitting in the same folder
(a manual roster, an Office lock file) can no longer crash the loader.
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.