-level background — the highlight has to go on each cell.
is_repost = rating == "Repost"
highlight = {"backgroundColor": "#fbdcdc", "textDecoration": "line-through"} if is_repost else {}
return html.Tr([
html.Td(date, style={**_nowrap, **highlight}),
html.Td(fmt, style={**_nowrap, **highlight}),
html.Td(eng, style={**_nowrap, **highlight}),
html.Td(post, title=post, style={**_post_style, **highlight}),
html.Td(html.Strong(rating), style={**_nowrap, **highlight}),
html.Td(why, style={"fontSize": "0.78rem", **highlight}),
])
_th_yellow = {"backgroundColor": "#fdf6d8"}
return dbc.Table([
html.Thead(html.Tr([html.Th(c, style={**(_nowrap if c not in ("Post", "Why") else {}), **_th_yellow})
for c in ["Date", "Format", "Real Eng %", "Post", "Rating", "Why"]])),
html.Tbody([_row(*row) for row in _JULY_REGISTER_RATINGS]),
], bordered=True, size="sm", style={"fontSize": "0.8rem", "marginTop": "14px", "tableLayout": "auto"})
def _rational_emotional_gauge(position_pct, caption):
"""Horizontal Rational<->Emotional indicator for the Formulate/Decision Context
read. position_pct (0=fully Rational, 100=fully Emotional) is a qualitative
placement per the "educated guess, not a computed stat" framing already
established for these dimensions — not a precise score. Light Blue/Orange: the
psychologically conventional cool-logical/warm-emotional pair, using the lighter
blue rather than the app's dominant navy so it reads as its own thing here
instead of blending into every header/border already using that navy."""
rational_color, emotional_color = LIGHT_BLUE, ORANGE
return html.Div([
html.Div([
html.Span("RATIONAL", style={"color": rational_color, "fontWeight": "700",
"fontSize": "0.7rem", "letterSpacing": "0.05em"}),
html.Span("EMOTIONAL", style={"color": emotional_color, "fontWeight": "700",
"fontSize": "0.7rem", "letterSpacing": "0.05em"}),
], style={"display": "flex", "justifyContent": "space-between", "marginBottom": "6px"}),
html.Div([
html.Div(caption, style={
"position": "absolute", "left": f"{position_pct}%", "top": "-20px",
"transform": "translateX(-50%)", "whiteSpace": "nowrap",
"fontSize": "0.72rem", "fontWeight": "700", "color": "#1a2533",
}),
html.Div(style={
"position": "absolute", "left": f"{position_pct}%", "top": "0px",
"transform": "translateX(-50%)", "width": "0", "height": "0",
"borderLeft": "7px solid transparent", "borderRight": "7px solid transparent",
"borderTop": "9px solid #1a2533",
}),
], style={"position": "relative", "height": "20px"}),
html.Div(style={
"height": "10px", "borderRadius": "5px",
"background": f"linear-gradient(90deg, {rational_color} 0%, {emotional_color} 100%)",
}),
], style={"width": "100%", "margin": "6px 0 16px"})
def _reports_card_body_style():
"""Flex column with space-between so an _insights_link sits flush against the
CardBody's own bottom padding (real breathing room before the card's border)
instead of wherever the content above happens to end."""
return {"display": "flex", "flexDirection": "column", "justifyContent": "space-between", "height": "100%"}
def _narrative_section(title, items):
if not items:
return html.Div()
return html.Div([
html.H5(title, style={"marginTop": "18px", "marginBottom": "8px", "color": "#1b3a5c"}),
html.Ul([html.Li(item, style={"marginBottom": "4px"}) for item in items],
style={"fontSize": "0.85rem", "paddingLeft": "1.1rem"}),
])
def _composition_table(counter, header_left, top_n=None, header_bg=None):
"""No truncation by default, sector/seniority/function are a small, fixed taxonomy
(see project memory's classification categories), and country is uncapped by
composition_stats() for the same reason, so showing everything keeps the table's
own sum matching the cohort total instead of silently dropping tail categories."""
items = counter.most_common(top_n) if hasattr(counter, "most_common") else list(counter)[:top_n]
th_style = {"backgroundColor": header_bg} if header_bg else {}
return dbc.Table([
html.Thead(html.Tr([html.Th(header_left, style=th_style), html.Th("Count", style=th_style)])),
html.Tbody([html.Tr([html.Td(k), html.Td(str(v))]) for k, v in items]),
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
def _reports_posts_table(posts_f):
"""Every post in the period, listed chronologically, not just a top-N by engagement.
A monthly campaign report should show the full picture of what was published, since
the point is a complete record, not a highlights reel. A repost/non-campaign date is
still shown here (not silently dropped), but struck through and marked "Excluded" —
same pale-red treatment as the dedicated All Posts insights page and Core Message's
register table, so a reader doesn't see a plain, unmarked row and wonder why its
Real Eng % looks unaudited instead of deliberately out of scope."""
if not len(posts_f):
return html.Div("No posts published in this period.", className="text-muted")
all_posts = posts_f.sort_values("Created date")
_post_title_style = {"whiteSpace": "nowrap", "overflow": "hidden", "textOverflow": "ellipsis",
"maxWidth": "260px"}
_excl_style = {"backgroundColor": "#fbdcdc", "textDecoration": "line-through"}
def _row(r):
excluded = r["Created date"].strftime("%Y-%m-%d") in (REPOST_DATES | NON_CAMPAIGN_DATES)
cell_style = {**_post_title_style, **_excl_style} if excluded else _post_title_style
plain_style = _excl_style if excluded else {}
eng_cell = (html.Span(["Excluded"], style={"fontWeight": "700"}) if excluded else
(f"{r['Real Eng %']:.1f}%" if pd.notna(r["Real Eng %"]) else
html.Span("Pending audit", style={"color": "#8a96a3", "fontStyle": "italic"})))
return html.Tr([
html.Td(str(r["Short title"]), title=str(r["Short title"]), style=cell_style),
html.Td(r["Created date"].strftime("%d/%m"), style=plain_style),
html.Td(r["Content Type"], style=plain_style),
html.Td(f"{int(r['Impressions']):,}", style=plain_style),
html.Td(f"{r['CTR %']:.1f}%", style=plain_style),
html.Td(eng_cell, style=plain_style),
])
return dbc.Table([
html.Thead(html.Tr([html.Th(c) for c in
["Post", "Date", "Type", "Impressions", "CTR %", "Real Eng %"]])),
html.Tbody([_row(r) for _, r in all_posts.iterrows()]),
], bordered=True, size="sm", style={"fontSize": "0.75rem"})
# Audience-vs-Team audit + Real Eng % now live in engagement_audit.py, shared with
# report.py (the PDF generator) — see that module's docstring for why it's separate.
_AUDIENCE_TEAM_AUDIT = engagement_audit.AUDIENCE_TEAM_AUDIT
_real_eng_pct = engagement_audit.real_eng_pct
def _reports_posts_f(month_value="2026-07"):
"""Shared by every Roadmap-family insights page — the selected month's posts with
Shares/Weekday columns added, so each page computes from the identical base
instead of drifting. Excludes REPOST_DATES (a reshare of someone else's post, not
TXproducts' own content) and NON_CAMPAIGN_DATES (a real TXproducts post, but not
part of the 24-post branding campaign) — neither belongs in a format/day/traction
breakdown of what the campaign itself published."""
stats = report.compute_period_stats(_build_report_ctx(), month_value)
posts_f = stats["posts_f"].copy()
excluded_dates = REPOST_DATES | NON_CAMPAIGN_DATES
posts_f = posts_f[~posts_f["Created date"].dt.strftime("%Y-%m-%d").isin(excluded_dates)]
posts_f["Shares"] = posts_f["Comments"].fillna(0) + posts_f["Reposts"].fillna(0)
posts_f["Weekday"] = posts_f["Created date"].dt.day_name()
return posts_f
_JULY_REGISTER_RATINGS = _july_register_ratings()
_FORMAT_ICONS = {
"Carousel": "bi-images",
"Video": "bi-camera-video",
"Text/Image": "bi-file-earmark-text",
"Poll": "bi-bar-chart-line",
}
_STAGE_SEQUENCE = [
("Traction", "bi-graph-up-arrow", "Find the Voice"),
("Demographics", "bi-people", "Find the Audience"),
("Conversion", "bi-arrow-left-right", "Read the Room"),
("Growth", "bi-rocket-takeoff", "Scale What Works"),
]
# Page 4's own one-stage-per-month mapping — which stage is "active" on the Roadmap
# timeline changes with whichever month is selected, not fixed to July/Traction.
_STAGE_FOR_MONTH = {"2026-07": "Traction", "2026-08": "Demographics", "2026-09": "Conversion"}
def _format_day_confound_note(posts_f):
"""Generic version of the Carousel/Thursday finding: if this month's data happens
to have every format landing on exactly one weekday (format and day perfectly
confounded), say so — computed from whatever month is selected, not hardcoded to
July's specific Carousel/Thursday pattern. Returns None when there's no confound
to report (not enough posts, or formats/days actually vary independently)."""
if len(posts_f) < 2 or posts_f["Weekday"].nunique() < 2:
return None
fmt_days = posts_f.groupby("Content Type")["Weekday"].agg(lambda s: set(s))
if not all(len(days) == 1 for days in fmt_days):
return None
day_fmts = {}
for fmt, days in fmt_days.items():
day_fmts.setdefault(next(iter(days)), []).append(fmt)
if len(day_fmts) < 2:
return None
clauses = [f"every {'/'.join(fmts)} post was on a {day}" for day, fmts in day_fmts.items()]
return html.P(
f"This month, {'; '.join(clauses)}, so the data can't isolate a real "
f"day-of-week effect from a format effect. Picking a day deliberately, "
f"independent of format, would let day-of-week actually be tested.",
style={"fontSize": "0.78rem", "color": "#8a3a3a", "margin": "10px 0 0",
"backgroundColor": "#fbdcdc", "padding": "8px 12px", "borderRadius": "6px"})
def _stage_timeline(active):
"""Page 4's own four-stage sequence (Traction/Demographics/Conversion/Growth),
styled like Core Message's flow diagram so the two pages read as one family."""
def _box(name, icon, subtitle):
# Active stage gets pale green rather than the app's own navy — navy is already
# the dominant color on every border/header here, so it wouldn't stand out as
# "current" the way a distinct accent does (same reasoning as the rational/
# emotional gauge's colors elsewhere on this page).
is_active = name == active
border_color = GREEN if is_active else BLUE
text_col = html.Div([
html.Div(name, style={"fontWeight": "700", "fontSize": "0.85rem", "color": BLUE}),
html.Div(subtitle, style={"fontSize": "0.68rem", "color": "#6b7a8d"}),
])
return html.Div([
html.I(className=f"bi {icon}", style={"fontSize": "2.1rem", "lineHeight": "1",
"color": border_color}),
text_col,
], style={
"display": "flex", "alignItems": "center", "gap": "12px",
"border": f"2px solid {border_color}", "borderRadius": "8px", "padding": "8px 16px",
"backgroundColor": "#eaf7e0" if is_active else "white",
})
def _arrow():
return html.Div(style={
"width": "0", "height": "0", "flexShrink": "0",
"borderTop": "9px solid transparent", "borderBottom": "9px solid transparent",
"borderLeft": f"13px solid {NAVY2}",
})
children = []
for i, (name, icon, subtitle) in enumerate(_STAGE_SEQUENCE):
if i:
children.append(_arrow())
children.append(_box(name, icon, subtitle))
return html.Div(children, style={"display": "flex", "alignItems": "center", "gap": "8px",
"flexWrap": "wrap"})
def _calendar_strip(highlight_abbr):
"""Mon-Fri pill strip echoing the Roadmap PDF's own page-2 calendar visual —
circles whichever day actually won in July instead of the document's Tuesday."""
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
def _pill(day):
is_hl = day == highlight_abbr
return html.Div(day, style={
"width": "56px", "height": "56px", "borderRadius": "50%", "flexShrink": "0",
"display": "flex", "alignItems": "center", "justifyContent": "center",
"fontSize": "0.85rem", "fontWeight": "700",
"border": f"2px solid {ORANGE if is_hl else '#e0e7ef'}",
"color": ORANGE if is_hl else "#6b7a8d",
"backgroundColor": "#fff5ef" if is_hl else "white",
})
return html.Div([_pill(d) for d in days], style={"display": "flex", "gap": "10px"})
# Spelled out fully, with a fake-but-concrete worked example, and reused verbatim
# everywhere Real Eng % has its own callout — "explain the formula" was specific,
# repeated feedback, not a one-off. Round numbers only, never a real post's, so it
# can never be mistaken for actual data no matter which post/month is on screen.
_REAL_ENG_FORMULA_EXAMPLE = html.Div([
html.Div([
html.Strong("Formula: "),
"(Audience Likes + Comments + Reposts + Clicks) ÷ Impressions × 100",
], style={"marginBottom": "6px"}),
html.Div(
"Example: 3 Likes + 1 Comment + 0 Reposts + 40 Clicks = 44 audience actions. "
"44 ÷ 200 Impressions = 0.22 → 22%.",
style={"color": "#6b7a8d", "fontSize": "0.82rem"}),
], style={"marginBottom": "10px", "paddingBottom": "10px", "borderBottom": "1px solid #e0e7ef"})
# Same treatment for CTR % — its own explicit formula/example, not just prose about
# what it means, plus the "Attention, not traffic" correction (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 — see engagement_audit.py's
# real_eng_pct docstring for the same reasoning applied to Real Eng %'s numerator).
_CTR_FORMULA_EXAMPLE = html.Div([
html.Div([
html.Strong("CTR % formula: "), "Clicks ÷ Impressions × 100.",
], style={"marginBottom": "6px"}),
html.Div("Example: 40 Clicks ÷ 200 Impressions = 20%.",
style={"color": "#6b7a8d", "fontSize": "0.82rem", "marginBottom": "8px"}),
html.Div([
html.Strong("This measures attention, not website traffic."), " LinkedIn never says "
"whether a click went to a website — TXproducts' links always live in a comment, never "
"the post itself, so these clicks are people expanding “see more,” viewing an "
"image, or swiping a carousel.",
], style={"color": "#6b7a8d"}),
])
# For tables that show both Real Eng % and CTR % but, unlike Engagement Composition,
# never had any explainer at all — Best Posting Day and Report Overview's posts list.
_REAL_ENG_AND_CTR_INFO = html.Div([_REAL_ENG_FORMULA_EXAMPLE, _CTR_FORMULA_EXAMPLE])
_FORMAT_BREAKDOWN_INFO = html.Div([
_REAL_ENG_FORMULA_EXAMPLE,
html.Div([
"Real Avg Eng % is ", html.Strong("Audience-only"),
", hand-audited against each post's actual Reactions/Comments/Reposts lists "
"on LinkedIn. Team activity is fully excluded.",
], style={"marginBottom": "8px"}),
html.Div("Clicks are included although unaudited.", style={"color": "#6b7a8d"}),
])
_ENG_OVER_TIME_INFO = html.Div([
_REAL_ENG_FORMULA_EXAMPLE,
html.Div([
"This chart plots Real Eng % per post. Each bar is ", html.Strong("Audience-only"), ".",
], style={"marginBottom": "8px"}),
html.Div("Clicks are included although unaudited.", style={"color": "#6b7a8d"}),
])
def _format_breakdown_table(posts_f):
"""Per-format stats with the actual constituent posts named inline, so the
breakdown answers "which posts belong there" in the same table, not a second
one — reposts already excluded upstream by _reports_posts_f(). Comments/Reposts
columns dropped — that detail (plus the Audience vs. Team split) lives on All
Posts' Engagement Composition card, not duplicated here. Real Eng % already
lives on posts_f (see compute_period_stats) — no LinkedIn-reported column here
at all, to avoid the exact "which number is real" confusion that motivated
dropping it."""
by_format = (posts_f.groupby("Content Type")
.agg(Posts=("Content Type", "size"),
**{"Real Avg Eng %": ("Real Eng %", "mean")})
.round(1).sort_values("Real Avg Eng %", ascending=False))
dates_by_format = (posts_f.sort_values("Created date")
.groupby("Content Type")["Created date"]
.apply(lambda s: ", ".join(d.strftime("%d/%m") for d in s)))
_th_yellow = {"backgroundColor": "#fdf6d8"}
rows = []
for fmt, r in by_format.iterrows():
rows.append(html.Tr([
html.Td(html.I(className=f"bi {_FORMAT_ICONS.get(fmt, 'bi-file-post')}",
style={"color": BLUE, "fontSize": "1rem"})),
html.Td(fmt, style={"fontWeight": "600", "whiteSpace": "nowrap"}),
html.Td(int(r["Posts"]), style={"whiteSpace": "nowrap"}),
html.Td(dates_by_format.get(fmt, ""), style={"fontSize": "0.78rem"}),
html.Td("Pending audit" if pd.isna(r["Real Avg Eng %"]) else f"{r['Real Avg Eng %']}%",
style={"whiteSpace": "nowrap", "fontWeight": "700",
"color": "#9a9a9a" if pd.isna(r["Real Avg Eng %"]) else "inherit",
"fontStyle": "italic" if pd.isna(r["Real Avg Eng %"]) else "normal"}),
]))
table = dbc.Table([
html.Thead(html.Tr([html.Th(c, style=_th_yellow) for c in
["", "Format", "Posts", "Dates", "Real Avg Eng %"]])),
html.Tbody(rows),
], bordered=True, size="sm", style={"fontSize": "0.82rem", "marginBottom": "8px"})
return html.Div([table, _engagement_composition_link()])
def _engagement_composition_link():
"""Same look as _insights_link — bold, right-aligned — pointing to All Posts'
Engagement Composition card, which has the per-post Comments/Reposts and the
full Audience vs. Team breakdown behind every Real Eng % figure on this page."""
return html.Div(
dcc.Link("Real Engagement breakdown → All Posts Insights",
href="/all-posts-insights",
style={"fontSize": "0.75rem", "fontWeight": "600", "color": "#2d6a9f",
"textDecoration": "none", "cursor": "pointer"}),
style={"textAlign": "right"})
def _weekday_detail_table(posts_f):
"""Full per-post metrics, chronological — same sort order as every other per-post
table in the app (Report Overview, All Posts, Core Message register), so cross-
referencing a specific post between them isn't confusing. The format/day confound
(e.g. every Carousel landing on the same weekday) is called out separately by
_format_day_confound_note's red-flag banner, not by grouping rows here."""
ordered = posts_f.sort_values("Created date")
_th_yellow = {"backgroundColor": "#fdf6d8"}
cols = ["Date", "Weekday", "Format", "Post", "Impressions", "Clicks", "CTR %", "Real Eng %", "Likes", "Comments", "Reposts"]
_post_style = {"whiteSpace": "nowrap", "overflow": "hidden", "textOverflow": "ellipsis", "maxWidth": "160px"}
rows = []
for _, r in ordered.iterrows():
real_eng = r["Real Eng %"]
rows.append(html.Tr([
html.Td(r["Created date"].strftime("%d/%m")),
html.Td(r["Weekday"]),
html.Td([html.I(className=f"bi {_FORMAT_ICONS.get(r['Content Type'], 'bi-file-post')}",
style={"color": BLUE, "marginRight": "6px"}), r["Content Type"]],
style={"whiteSpace": "nowrap"}),
html.Td(str(r["Short title"]), title=str(r["Short title"]), style=_post_style),
html.Td(f"{int(r['Impressions']):,}"),
html.Td(int(r["Clicks"])),
html.Td(f"{r['CTR %']}%"),
html.Td(f"{real_eng:.1f}%" if pd.notna(real_eng) else
html.Span("Pending audit", style={"color": "#9a9a9a", "fontStyle": "italic"})),
html.Td(int(r["Likes"])),
html.Td(int(r["Comments"])),
html.Td(int(r["Reposts"])),
]))
return dbc.Table([
html.Thead(html.Tr([html.Th(c, style=_th_yellow) for c in cols])),
html.Tbody(rows),
], bordered=True, size="sm", style={"fontSize": "0.78rem", "marginBottom": "0"})
def _roadmap_insights_page(month_value="2026-07"):
"""Grounded in Campaign Roadmap.pdf pages 2-5: the Tuesday/Thursday cadence claim
(p.2), CTA/traction-by-format claims (p.3), the four-stage sequence (p.4), and
each month's own "checkpoint" (p.5) — read against that month's real data, not
as a planned-vs-actual grade against a pre-campaign sketch the team has already
said was loosely thought out ("we are just learning"). Reposts excluded
everywhere via _reports_posts_f() (REPOST_DATES) — a reshare of someone else's
post isn't TXproducts' own content in any format sense. Real Avg Eng % and Real
Engagement Over Time both fall back to "Pending audit"/reported-only for any
month that hasn't been hand-audited yet — only July has been so far."""
posts_f = _reports_posts_f(month_value)
period_label = report._period_label(month_value)
active_stage = _STAGE_FOR_MONTH.get(month_value, "Traction")
stage_index = next((i for i, (name, *_r) in enumerate(_STAGE_SEQUENCE) if name == active_stage), 0)
if posts_f.empty:
return dbc.Container([
dbc.Alert(f"No posts published in {period_label} yet.", color="info"),
], fluid=True, className="pb-3")
# Real Eng % already lives on posts_f (see compute_period_stats). month_audited
# gates the "best day"/chart fallback to LinkedIn's raw Eng % only when NOTHING
# this month has been audited yet — a partially-audited month still means among
# its audited posts, since groupby().mean() already skips NaN per row.
month_audited = posts_f["Real Eng %"].notna().any()
eng_col = "Real Eng %" if month_audited else "Eng %"
by_day = (posts_f.groupby("Weekday").agg(**{"Avg Eng %": (eng_col, "mean")}).round(1))
best_day = by_day["Avg Eng %"].idxmax()
timeline_card = dbc.Card([
dbc.CardHeader(html.Span("Branding Campaign's Four Stages in Sequence (p.4)",
style={"color": "#FFD54F"})),
dbc.CardBody(
html.Div([
_stage_timeline(active_stage),
html.P(
f"The Roadmap lays out one stage per month. {period_label} is "
f"Stage {stage_index + 1}, {active_stage}"
+ (" — the rest follow later in the campaign." if stage_index < 3
else " — the campaign's final stage."),
style={"fontSize": "0.85rem", "margin": "0", "flex": "1", "minWidth": "260px"}),
], style={"display": "flex", "alignItems": "center", "gap": "24px", "flexWrap": "wrap"})
),
], className="mb-3 shadow-sm")
format_card = dbc.Card([
dbc.CardHeader([html.Span("Traction by Format (p.3)", style={"color": "#FFD54F"}),
info_popover("format-breakdown", _FORMAT_BREAKDOWN_INFO)]),
dbc.CardBody(_format_breakdown_table(posts_f)),
], className="shadow-sm h-100")
confound_note = _format_day_confound_note(posts_f)
day_card = dbc.Card([
dbc.CardHeader([html.Span("Best Posting Day (p.2)" if month_audited
else "Best Posting Day (p.2) (Pending Audit)",
style={"color": "#FFD54F"}),
info_popover("best-posting-day", _REAL_ENG_AND_CTR_INFO)]),
dbc.CardBody([
html.Div([
_calendar_strip(best_day[:3]),
html.P([
html.Strong(best_day), " shows the highest average engagement this month.",
], style={"fontSize": "0.85rem", "margin": "0", "flex": "1", "minWidth": "200px"}),
], style={"display": "flex", "alignItems": "center", "gap": "24px", "flexWrap": "wrap",
"marginBottom": "14px"}),
html.P(f"{period_label} hasn't been hand-audited yet — showing LinkedIn's "
"reported Eng %, Team activity included.",
style={"fontSize": "0.76rem", "color": "#9a9a9a", "fontStyle": "italic",
"margin": "0 0 10px"}) if not month_audited else None,
_weekday_detail_table(posts_f),
] + ([confound_note] if confound_note is not None else [])),
], className="mb-3 shadow-sm")
posts_f_sorted = posts_f.sort_values("Created date")
eng_fig = px.bar(posts_f_sorted, x="Created date",
y="Real Eng %" if month_audited else "Eng %",
color="Content Type", color_discrete_map=FORMAT_COLORS,
template=TMPL, labels={"Content Type": "Format", "Created date": "",
"Real Eng %": "", "Eng %": ""})
eng_fig = _std(eng_fig)
eng_fig.update_layout(margin=dict(t=36, b=20, l=10, r=10),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0,
font=dict(size=9), title=None))
year, month = (int(p) for p in month_value.split("-"))
range_start = f"{year}-{month:02d}-01"
range_end = f"{year + 1}-01-01" if month == 12 else f"{year}-{month + 1:02d}-01"
eng_fig.update_xaxes(range=[range_start, range_end], tickformat="%b %d", fixedrange=True)
eng_fig.update_yaxes(fixedrange=True, ticksuffix="%")
y_col = "Real Eng %" if month_audited else "Eng %"
eng_fig.add_trace(go.Scatter(
x=posts_f_sorted["Created date"], y=posts_f_sorted[y_col], mode="markers", showlegend=False,
marker=dict(symbol="star", size=11, color="#E53935", line=dict(width=1, color="white")),
name="Post published", customdata=posts_f_sorted["Short title"],
hovertemplate="Post published — %{x|%b %d, %Y}
%{customdata}",
))
growth_card = dbc.Card([
dbc.CardHeader([html.Span("Real Engagement Over Time" if month_audited
else "Engagement Over Time (Pending Audit)",
style={"color": "#FFD54F"}),
info_popover("eng-over-time", _ENG_OVER_TIME_INFO)]),
dbc.CardBody([
G(eng_fig, style={"height": "170px"}),
html.P(f"{period_label} hasn't been hand-audited yet — showing LinkedIn's "
"reported Eng %, Team activity included.",
style={"fontSize": "0.76rem", "color": "#9a9a9a", "fontStyle": "italic",
"margin": "4px 0 0"}) if not month_audited else None,
_engagement_composition_link(),
]),
], className="shadow-sm")
main_row = html.Div([format_card, growth_card],
style={"display": "grid", "gridTemplateColumns": "3fr 2fr", "gap": "1rem", "marginBottom": "1rem"})
return dbc.Container([timeline_card, main_row, day_card], fluid=True, className="pb-3")
_ENGAGEMENT_CELL_WIDTH = "128px"
_AUDIENCE_COLOR = "#fbe6d2" # very pale orange
_TEAM_COLOR = "#dbe9f8" # very pale blue
def _audience_team_td(audience_n, team_n):
total = audience_n + team_n
audience_pct = (audience_n / total * 100) if total else 0
team_pct = (team_n / total * 100) if total else 0
# The itself carries the diagonal split — two triangles via clip-path
# (not a fixed-angle gradient, which cuts corner-to-corner only for a square;
# clip-path polygons always scale to the cell's actual width/height, so the
# dividing line reaches true corner to corner no matter the cell's shape).
# No A/T letters inside the triangles — the legend above already maps color to
# meaning, so the number can just be the number. Wider/shorter box (elongated)
# keeps the text on one line at a shorter cell height. Module-level (not just
# All Posts' own helper) so Best Posting Day/Content tab can reuse it too.
return html.Td([
html.Div(style={
"position": "absolute", "inset": "0", "backgroundColor": _AUDIENCE_COLOR,
"clipPath": "polygon(0% 0%, 100% 0%, 0% 100%)"}),
html.Div(style={
"position": "absolute", "inset": "0", "backgroundColor": _TEAM_COLOR,
"clipPath": "polygon(100% 0%, 100% 100%, 0% 100%)"}),
html.Div(f"{audience_n}", style={
"position": "absolute", "top": "3px", "left": "5px", "fontSize": "0.78rem",
"color": "#5a3a1a", "lineHeight": "1", "whiteSpace": "nowrap", "zIndex": "1"}),
html.Div(f"{team_n}", style={
"position": "absolute", "bottom": "3px", "right": "5px", "fontSize": "0.78rem",
"color": "#1a3a5a", "lineHeight": "1", "whiteSpace": "nowrap", "zIndex": "1"}),
], title=f"Audience: {audience_n} ({audience_pct:.0f}%) · Team: {team_n} ({team_pct:.0f}%)", style={
"position": "relative", "padding": "0", "height": "26px",
"width": _ENGAGEMENT_CELL_WIDTH, "minWidth": _ENGAGEMENT_CELL_WIDTH,
"maxWidth": _ENGAGEMENT_CELL_WIDTH})
def _most_reach_post(posts_f):
"""Whichever audited post has the highest combined Audience Acknowledgment
(Likes) + Conversation (Comments) + Amplification (Reposts) this month — the
Read column's own three categories, summed. Not Real Eng % (dominated by
Clicks, which have no ripple/reach potential at all since nobody but the
clicker ever sees it) and not raw Reposts alone (ties too easily, e.g. July's
02/07 vs 30/07 tie at 2 each). Unaudited posts aren't eligible — there's
nothing to compare. Returns None if no post this month has been audited."""
best_row, best_total = None, -1
for _, r in posts_f.iterrows():
date_ddmm = r["Created date"].strftime("%d/%m")
audit = _AUDIENCE_TEAM_AUDIT.get(date_ddmm)
if not audit:
continue
total = (audit["likes"]["audience"] + audit["comments"]["audience"]
+ audit["reposts"]["audience"])
if total > best_total:
best_total, best_row = total, r
return None if best_row is None else (best_row, best_total)
def _most_reach_sticker(posts_f):
"""Sleek circular badge, not a chart — a pointer to which row to read, not a
second place to read numbers (the old composition chart's problem). Shows only
the winning post's format icon and date; every actual number stays in the
table itself. Returns an empty Div if nothing this month is audited yet."""
result = _most_reach_post(posts_f)
if result is None:
return html.Div()
r, _total = result
icon_class = _FORMAT_ICONS.get(r["Content Type"], "bi-file-post")
return html.Div([
html.Div("Most Reach", style={
"fontSize": "0.75rem", "fontWeight": "800", "textTransform": "uppercase",
"letterSpacing": "0.05em", "color": "#8a6d1a", "marginBottom": "14px"}),
# A medallion, not a flat dot: an outer rim (darker gold ring) with an inner
# disc (lighter gold-to-amber gradient) inset inside it, plus a subtle inset
# highlight — the layering is what reads as "medal" rather than "sticker,"
# without resorting to a literal trophy/ribbon icon.
html.Div(
html.Div([
html.I(className=f"bi {icon_class}", style={"fontSize": "1.8rem", "color": "#6b4e00"}),
html.Div(r["Created date"].strftime("%d/%m"), style={
"fontWeight": "800", "fontSize": "1.3rem", "color": "#6b4e00",
"marginTop": "4px", "lineHeight": "1"}),
], style={
"width": "104px", "height": "104px", "borderRadius": "50%",
"background": "radial-gradient(circle at 35% 30%, #FFEDA8 0%, #FFC947 55%, #E8A317 100%)",
"display": "flex", "flexDirection": "column", "alignItems": "center", "justifyContent": "center",
"boxShadow": "inset 0 0 0 3px rgba(255,255,255,0.5), inset 0 -4px 8px rgba(150,100,0,0.25)",
}),
style={
"width": "124px", "height": "124px", "borderRadius": "50%",
"backgroundColor": "#C9960C", "display": "flex", "alignItems": "center", "justifyContent": "center",
"boxShadow": "0 4px 10px rgba(0,0,0,0.22)",
}),
], style={"textAlign": "center", "display": "flex", "flexDirection": "column", "alignItems": "center"})
def _all_posts_insights_page(month_value="2026-07"):
"""Mirrors the Content tab itself, scoped to the selected month: the post list
first, then the same format-breakdown and daily-activity charts, in the same
order Content shows them — Traction by Format is Roadmap's own table now (with
dates/reposts split out), so it isn't repeated here."""
stats = report.compute_period_stats(_build_report_ctx(), month_value)
period_label = stats["period_label"]
posts_f_all = stats["posts_f"]
cts_f = stats["cts_f"]
if posts_f_all.empty:
return dbc.Container([
dbc.Alert(f"No posts published in {period_label} yet.", color="info"),
], fluid=True, className="pb-3")
kpi_row = html.Div([
html.Div(kpi("Posts Published", f"{stats['posts_published']:,}"), style={"flex": "1", "minWidth": "0"}),
html.Div(kpi("Total Impressions", f"{stats['total_impressions']:,}"), style={"flex": "1", "minWidth": "0"}),
html.Div(kpi("Best Post Real Eng.", _fmt_best_eng(stats["best_eng"]), color=GREEN), style={"flex": "1", "minWidth": "0"}),
html.Div(kpi("Avg. Eng. Rate", f"{stats['avg_eng']:.2f}%" if stats["avg_eng"] else "—",
"raw, unaudited"),
style={"flex": "1", "minWidth": "0"}),
], style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"})
_comp_df = _reports_posts_f(month_value).sort_values("Created date").copy()
# _reports_posts_f() correctly excludes REPOST_DATES from every calculation, but
# per Mckinley's request the repost should still be VISIBLE here, struck through
# and marked Excluded, not silently missing from the post list — same
# transparency treatment Core Message's register table already gives it.
_repost_rows = posts_f_all[posts_f_all["Created date"].dt.strftime("%Y-%m-%d").isin(REPOST_DATES)]
if not _repost_rows.empty:
_comp_df = pd.concat([_comp_df, _repost_rows]).sort_values("Created date")
_comp_df["Date"] = _comp_df["Created date"].dt.strftime("%d/%m")
def _first_n_words(text, n=5):
words = str(text).split()
return " ".join(words[:n]) + ("..." if len(words) > n else "")
_th_yellow_comp = {"backgroundColor": "#fdf6d8"}
_post_style_comp = {"whiteSpace": "nowrap", "overflow": "hidden", "textOverflow": "ellipsis", "maxWidth": "90px"}
_type_abbrev = {"Text/Image": "T", "Carousel": "C", "Video": "V", "Poll": "P"}
def _comp_row(r):
if r["Created date"].strftime("%Y-%m-%d") in REPOST_DATES:
# Visible, not silently dropped, but struck through and clearly marked —
# same pale-red/strikethrough treatment as Core Message's register table.
excl = {"backgroundColor": "#fbdcdc", "textDecoration": "line-through"}
return html.Tr([
html.Td([_first_n_words(r["Short title"], 3), " ", html.Strong("Excluded")],
title=str(r["Short title"]), style={**_post_style_comp, **excl}),
html.Td(_type_abbrev.get(r["Content Type"], r["Content Type"]),
title=r["Content Type"], style={"whiteSpace": "nowrap", "textAlign": "center", **excl}),
html.Td(r["Date"], style={"whiteSpace": "nowrap", **excl}),
html.Td(f"{int(r['Impressions']):,}", style={"whiteSpace": "nowrap", **excl}),
html.Td(int(r["Clicks"]), style={"whiteSpace": "nowrap", **excl}),
html.Td(f"{r['CTR %']:.1f}%", style={"whiteSpace": "nowrap", **excl}),
html.Td("Excluded", style={"whiteSpace": "nowrap", **excl}),
html.Td(int(r["Likes"]), style={"textAlign": "center", **excl}),
html.Td(int(r["Comments"]), style={"textAlign": "center", **excl}),
html.Td(int(r["Reposts"]), style={"textAlign": "center", **excl}),
html.Td("Repost, excluded from comparison", style={"fontSize": "0.76rem", **excl}),
])
audit = _AUDIENCE_TEAM_AUDIT.get(r["Date"])
if audit:
# Plain counts, not percentages — a share-of-total on a small organic base
# (e.g. 7 Likes/0/0) inflates into a misleading "100%", the same small-
# denominator distortion already flagged for the Poll. Counts can't inflate.
read = (f"{audit['likes']['audience']} Acknowledgment / "
f"{audit['comments']['audience']} Conversation / "
f"{audit['reposts']['audience']} Amplification")
likes_cell = _audience_team_td(audit["likes"]["audience"], audit["likes"]["team"])
comments_cell = _audience_team_td(audit["comments"]["audience"], audit["comments"]["team"])
reposts_cell = _audience_team_td(audit["reposts"]["audience"], audit["reposts"]["team"])
else:
# No rule-based label here — the old max-share "Balanced/Passive/..." rule
# was already found misleading on small totals (the Poll) and dropped in
# favor of plain counts for audited posts. An unaudited post gets the same
# treatment now too: the raw count, not a share-of-total percentage — same
# small-denominator reasoning, and it keeps the column's units consistent
# (a number, split or not) rather than switching to a percentage only when
# unaudited.
read = "Pending audit"
_plain_style = {"whiteSpace": "nowrap", "textAlign": "center", "width": _ENGAGEMENT_CELL_WIDTH,
"minWidth": _ENGAGEMENT_CELL_WIDTH, "maxWidth": _ENGAGEMENT_CELL_WIDTH}
likes_cell = html.Td(int(r["Likes"]), style=_plain_style)
comments_cell = html.Td(int(r["Comments"]), style=_plain_style)
reposts_cell = html.Td(int(r["Reposts"]), style=_plain_style)
real_eng = r["Real Eng %"]
real_eng_cell = (html.Td(f"{real_eng:.1f}%", style={"whiteSpace": "nowrap", "fontWeight": "700"})
if pd.notna(real_eng) else
html.Td("Pending audit", style={"whiteSpace": "nowrap", "color": "#9a9a9a",
"fontStyle": "italic"}))
return html.Tr([
html.Td(_first_n_words(r["Short title"], 3), title=str(r["Short title"]), style=_post_style_comp),
html.Td(_type_abbrev.get(r["Content Type"], r["Content Type"]), title=r["Content Type"],
style={"whiteSpace": "nowrap", "textAlign": "center"}),
html.Td(r["Date"], style={"whiteSpace": "nowrap"}),
html.Td(f"{int(r['Impressions']):,}", style={"whiteSpace": "nowrap"}),
html.Td(int(r["Clicks"]), style={"whiteSpace": "nowrap"}),
html.Td(f"{r['CTR %']:.1f}%", style={"whiteSpace": "nowrap"}),
real_eng_cell,
likes_cell,
comments_cell,
reposts_cell,
html.Td(read, style={"fontSize": "0.76rem"}),
])
_comp_legend = html.Div([
html.Span(style={"display": "inline-block", "width": "10px", "height": "10px",
"backgroundColor": _AUDIENCE_COLOR, "borderRadius": "2px", "marginRight": "4px"}),
html.Span("A = Audience", style={"marginRight": "12px", "fontSize": "0.72rem"}),
html.Span(style={"display": "inline-block", "width": "10px", "height": "10px",
"backgroundColor": _TEAM_COLOR, "borderRadius": "2px", "marginRight": "4px"}),
html.Span("T = Team", style={"marginRight": "12px", "fontSize": "0.72rem"}),
html.Span("Type: T=Text/Image, C=Carousel, V=Video, P=Poll", style={"fontSize": "0.72rem", "color": "#6b7a8d"}),
], style={"marginBottom": "6px"})
_comp_table = html.Div([
_comp_legend,
dbc.Table([
html.Thead(html.Tr([html.Th(c, style=_th_yellow_comp) for c in
["Post", "Type", "Date", "Impressions", "Clicks", "CTR %", "Real Eng %",
"Likes", "Comments", "Reposts", "Read (Audience only, Team excluded)"]])),
html.Tbody([_comp_row(r) for _, r in _comp_df.iterrows()]),
], bordered=True, size="sm", style={"fontSize": "0.78rem", "marginBottom": "0", "width": "100%"}),
], style={"flex": "1", "minWidth": "0"})
_engagement_composition_info_content = html.Div([
_REAL_ENG_FORMULA_EXAMPLE,
html.Div([
html.Strong("Real Eng % is hand-audited."), " Likes, Comments, and Reposts are split "
"Audience vs Team — Team (TXproducts staff) are excluded from the formula.",
], style={"marginBottom": "8px"}),
html.Div([
html.Strong("Impressions, Clicks, CTR %"), " stay plain, never split — these can't be "
"audited, since LinkedIn never reveals who clicked or was shown a post.",
], style={"marginBottom": "8px", "color": "#6b7a8d"}),
html.Div([
"Only ", html.Strong("Audience"), " numbers feed Real Eng % and the Read. Unaudited "
"posts show ", html.Strong("Pending audit"), ".",
], style={"marginBottom": "10px", "color": "#6b7a8d"}),
_CTR_FORMULA_EXAMPLE,
])
_reach_sticker = _most_reach_sticker(_comp_df)
composition_card = section(
"Engagement Composition",
html.Div([
_comp_table,
] + ([
html.Div(style={"width": "1px", "backgroundColor": "#e0e7ef", "margin": "0 20px",
"alignSelf": "stretch"}),
html.Div(_reach_sticker, style={"flexShrink": "0", "display": "flex", "alignItems": "center"}),
] if _reach_sticker.children else []),
style={"display": "flex", "alignItems": "stretch"}),
info=("engagement-composition", _engagement_composition_info_content),
title_style={"fontSize": "0.78rem"},
)
_impressions_fmt_fig = impressions_by_format_figure(posts_f_all)
_impressions_fmt_fig.update_xaxes(title=None)
_engagement_fmt_fig = engagement_by_format_figure(posts_f_all)
_engagement_fmt_fig.update_xaxes(title=None)
_ctr_fmt_fig = ctr_by_format_figure(posts_f_all)
_ctr_fmt_fig.update_xaxes(title=None)
_scatter_fig = impressions_vs_engagement_figure(posts_f_all)
mirror_row1 = dbc.Row([
dbc.Col(section("Impressions by Format",
G(_impressions_fmt_fig, style={"height": "180px"},
margin=CONTENT_BAR_MARGIN), title_style={"fontSize": "0.72rem"}), md=3),
dbc.Col(section("Avg. Engagement Rate by Format",
G(_engagement_fmt_fig, style={"height": "180px"},
margin=CONTENT_BAR_MARGIN), title_style={"fontSize": "0.72rem"}), md=3),
dbc.Col(section("Avg. CTR by Format",
G(_ctr_fmt_fig, style={"height": "180px"},
margin=CONTENT_BAR_MARGIN), title_style={"fontSize": "0.72rem"}), md=3),
dbc.Col(section("Impressions vs Engagement Rate",
G(_scatter_fig, style={"height": "180px"}, legend=CONTENT_SCATTER_LEGEND),
title_style={"fontSize": "0.72rem"}), md=3),
], className="gx-3 mb-3")
_reactions_fig = reactions_figure(cts_f)
_reactions_fig.update_xaxes(title=None, tickformat="%b %d")
_clicks_fig = clicks_figure(cts_f)
_clicks_fig.update_xaxes(title=None, tickformat="%b %d")
mirror_row2 = dbc.Row([
dbc.Col(section(f"Daily Reactions, Comments & Reposts, {period_label}",
G(_reactions_fig, style={"height": "180px"})), md=6),
dbc.Col(section(f"Daily Clicks, {period_label}",
G(_clicks_fig, style={"height": "180px"})), md=6),
], className="gx-3 mb-3")
return dbc.Container([
kpi_row,
composition_card,
mirror_row1,
mirror_row2,
], fluid=True, className="pb-3")
def _follower_growth_insights_page(month_value="2026-07"):
"""Mirrors the Overview tab's own Follower Gains calendar and New Followers per
Day chart, plus the Growth tab's Total Followers Over Time and Rolling Best
charts — all pinned to the selected month (no year/month picker, no History Log
table). The calendar drops zero-gain post days (include_zero_gain_posts=False):
this page is scoped to growth days specifically, not the combined post+growth
view Overview/Followers show."""
stats = report.compute_period_stats(_build_report_ctx(), month_value)
period_label = stats["period_label"]
year, month = (int(p) for p in month_value.split("-"))
kpi_row = html.Div([
html.Div(kpi("Month Start",
f"{stats['start_of_month_followers']:,}" if stats["start_of_month_followers"] is not None else "—"),
style={"flex": "1", "minWidth": "0"}),
html.Div(kpi("New Followers", f"{stats['new_followers']:,}", color=ORANGE),
style={"flex": "1", "minWidth": "0"}),
html.Div(kpi("Growth Rate",
f"{stats['monthly_growth_rate']:+.1f}%" if stats["monthly_growth_rate"] is not None else "—",
color=GREEN if (stats["monthly_growth_rate"] or 0) >= 0 else "#d43f3a"),
style={"flex": "1", "minWidth": "0"}),
], style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"})
mirror_row = dbc.Row([
dbc.Col(section(f"Follower Gains — Calendar, {period_label} (growth days only)",
html.Div([
_month_day_tiles(D["fts"], D["posts"], year, month, include_zero_gain_posts=False),
calendar_legend(),
], style={"height": "270px", "display": "flex", "flexDirection": "column"})
), md=8),
dbc.Col(section(f"New Followers per Day, {period_label.split()[0]} (★ = post)",
G(daily_followers_figure(D["fts"], D["posts"], year, month), style={"height": "270px"})
), md=4),
], className="gx-3 mb-3")
growth_history = growth.load_history(UPLOAD_DIR)
month_history = [h for h in growth_history if h["date"].startswith(month_value)]
if not month_history:
growth_row = dbc.Alert(f"No growth-tracking history recorded for {period_label} yet.", color="info")
else:
growth_row = dbc.Row([
dbc.Col(dbc.Card([
dbc.CardHeader(f"Total Followers Over Time, {period_label}"),
dbc.CardBody(G(growth_trend_figure(month_history), style={"height": "260px"})),
], className="mb-3 shadow-sm"), md=6),
dbc.Col(dbc.Card([
dbc.CardHeader([html.Strong(f"Rolling Best, {period_label}"),
info_popover("fg-insights-growth-pace", GROWTH_OBJECTIVE_EXPLAINER)]),
dbc.CardBody(G(growth_pace_figure(month_history), style={"height": "260px"})),
], className="mb-3 shadow-sm"), md=6),
])
return dbc.Container([
kpi_row,
mirror_row,
growth_row,
], fluid=True, className="pb-3")
def _core_message_insights_page(month_value="2026-07"):
"""Evaluates the selected month against the Core Message document itself
(Core_Message.pdf), not as a compliance check but as the document's own revision
cycle in action — it says its own Analyze/Extract/Formulate/Validate steps are
"also the framework for revising it" (p.2) once real audience response comes in.
Structure, agreed by brainstorm 2026-08-07, corrected 2026-08-08 (p.8 is
Methodology feeding Formulate, not its own step — the real 4 steps are
p.3/p.4/p.5/p.6):
p.2 four-step framework + the 5 documented revisit triggers
p.3 ANALYZE — the original audience/differentiator guess (made without deep
industry research) vs. who actually showed up. Same Directory data as the
Audience Composition card/page and Follower Study — worth naming that
overlap rather than presenting it as independent evidence. Fully
computable per month via the Directory.
p.4 EXTRACT, p.5 FORMULATE, p.6 VALIDATE — all three are hand-curated per
month (trilogy identification, Rational/Emotional register
classification, validation check), the same kind of manual work behind
the Follower Directory and the Audience-vs-Team audit. Only July has
been done so far; other months show a plain "not yet written up" card
instead of pretending July's specific findings apply elsewhere.
"""
period_label = report._period_label(month_value)
is_july = month_value == "2026-07"
cohort = follower_directory_data.composition_stats(DIRECTORY_DATA, month_value)
_th_yellow = {"backgroundColor": "#fdf6d8"}
func_table = dbc.Table([
html.Thead(html.Tr([html.Th(f"Function ({period_label} cohort)", style=_th_yellow),
html.Th("Count", style=_th_yellow)])),
html.Tbody([html.Tr([html.Td(k), html.Td(str(v))]) for k, v in cohort["function"].most_common()]),
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
sector_table = dbc.Table([
html.Thead(html.Tr([html.Th(f"Sector ({period_label} cohort)", style=_th_yellow),
html.Th("Count", style=_th_yellow)])),
html.Tbody([html.Tr([html.Td(k), html.Td(str(v))]) for k, v in cohort["sector"].most_common()]),
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
_CORE_MESSAGE_ICONS = {
"Analyze": "bi-search", "Extract": "bi-funnel",
"Formulate": "bi-chat-quote", "Validate": "bi-clipboard-check",
}
def _flow_box(label):
return html.Div([
html.I(className=f"bi {_CORE_MESSAGE_ICONS[label]}",
style={"fontSize": "1.4rem", "color": BLUE}),
html.Div(label, style={"color": BLUE, "fontWeight": "600", "fontSize": "0.95rem"}),
], style={
"display": "flex", "alignItems": "center", "gap": "8px",
"border": f"2px solid {BLUE}", "borderRadius": "8px", "padding": "8px 18px",
"backgroundColor": "white",
})
def _flow_arrow():
return html.Div(style={
"width": "0", "height": "0", "flexShrink": "0",
"borderTop": "11px solid transparent", "borderBottom": "11px solid transparent",
"borderLeft": f"15px solid {NAVY2}",
})
flow_diagram = html.Div([
_flow_box("Analyze"), _flow_arrow(),
_flow_box("Extract"), _flow_arrow(),
_flow_box("Formulate"), _flow_arrow(),
_flow_box("Validate"),
], style={"display": "flex", "alignItems": "center", "gap": "10px", "flexWrap": "wrap"})
framework_card = dbc.Card([
dbc.CardHeader(html.Span("Four Step Core Message Framework", style={"color": "#FFD54F"})),
dbc.CardBody(
html.Div([
flow_diagram,
html.P(
"These four steps were used to develop the messaging. They are also "
"the framework for revising it.",
style={"fontSize": "0.85rem", "textAlign": "center", "margin": "0"}),
], style={"display": "flex", "alignItems": "center", "gap": "24px", "flexWrap": "wrap"})
),
], className="mb-3 shadow-sm")
audience_card = dbc.Card([
dbc.CardHeader(html.Span("ANALYZE: Who is the target audience and what is their "
"problem? (p.3)", style={"color": "#FFD54F"})),
dbc.CardBody([
html.P(
(f"{cohort['sector'].most_common(1)[0][0]} is the largest single sector "
"this month." if cohort["total"] else f"No {period_label} cohort profiled yet."),
style={"fontSize": "0.85rem", "marginBottom": "10px"}),
dbc.Row([dbc.Col(sector_table, md=6), dbc.Col(func_table, md=6)]),
]),
], className="shadow-sm h-100")
if not is_july:
_not_yet_written_up = dbc.CardBody(
html.P(f"Not yet written up for {period_label} — this section is hand-curated "
"per month (trilogy identification, Rational/Emotional register "
"classification, validation check), the same kind of manual work behind "
"the Audience-vs-Team audit. Only July has been done so far.",
style={"fontSize": "0.85rem", "color": "#9a9a9a", "fontStyle": "italic"}))
extract_card = dbc.Card([
dbc.CardHeader(html.Span("EXTRACT: The Formula and the Final Message (p.4)",
style={"color": "#FFD54F"})),
_not_yet_written_up,
], className="shadow-sm h-100")
register_card = dbc.Card([
dbc.CardHeader(html.Span("FORMULATE: Boilerplate Register (p.5)", style={"color": "#FFD54F"})),
_not_yet_written_up,
], className="shadow-sm h-100")
validate_card = dbc.Card([
dbc.CardHeader(html.Span("VALIDATE (p.6)", style={"color": "#FFD54F"})),
_not_yet_written_up,
], className="shadow-sm h-100")
analyze_extract_row = html.Div(
[audience_card, extract_card],
style={"display": "grid", "gridTemplateColumns": "1fr 1fr", "gap": "1rem", "marginBottom": "1rem"},
)
formulate_validate_row = html.Div(
[register_card, validate_card],
style={"display": "grid", "gridTemplateColumns": "4fr 1fr", "gap": "1rem", "marginBottom": "1rem"},
)
return dbc.Container([framework_card, analyze_extract_row, formulate_validate_row],
fluid=True, className="pb-3")
extract_card = dbc.Card([
dbc.CardHeader(html.Span("EXTRACT: The Formula and the Final Message (p.4)",
style={"color": "#FFD54F"})),
dbc.CardBody([
html.P(
"Focusing on \"SO THAT\" from page 4's formula, as this is the part of the "
"message meant to speak directly to the audience.",
style={"fontSize": "0.85rem"}),
html.P(
"We seem to have something closer to an actual playbook where we clearly "
"see the value proposition.",
style={"fontSize": "0.85rem"}),
html.Ul([
html.Li([html.Strong("14/07"), " — introduces the problem (operation modes, "
"frustration of waiting)"]),
html.Li([html.Strong("28/07"), " — deepens it with the Ferris wheel analogy, "
"ends on a cliffhanger CTA"]),
html.Li([html.Strong("30/07"), " — delivers the solution, in Daniel's own "
"voice, closing the loop"]),
], style={"fontSize": "0.85rem"}),
html.P(
"The trilogy of three posts brings in honesty in a way that matters for "
"credibility, further underlined by technical substance delivered through "
"a personal-voice format.",
style={"fontSize": "0.85rem"}),
]),
], className="shadow-sm h-100")
register_left = html.Div([
_rational_emotional_gauge(75, "July"),
_july_register_table(),
], style={"paddingRight": "16px", "borderRight": "1px solid #e0e7ef"})
register_right = html.Div([
html.Div([
_dimension_card("Prior Knowledge and Language", [
"This cohort is mostly Academic and Engineering/Technical and understands "
"the underlying science.",
]),
_dimension_card("Decision Context", [
"Most of this cohort do not have a budget-holding role, so they're not "
"positioned to act on a rational business case.",
]),
_dimension_card("Trust and Reservations", [
"Emotional posts outperformed the rational ones. Leading with a human touch "
"to a skeptical audience wins.",
]),
], style={"display": "flex", "flexDirection": "column", "gap": "12px"}),
], style={"paddingLeft": "16px"})
register_card = dbc.Card([
dbc.CardHeader(html.Span(
"FORMULATE: Boilerplate Register (p.5), Analytical Dimensions Methodology "
"(p.8) - (applied as interpretive judgment calls, not computed stats)",
style={"color": "#FFD54F"})),
dbc.CardBody(
html.Div([register_left, register_right],
style={"display": "grid", "gridTemplateColumns": "2fr 1fr", "gap": "0"})
),
], className="shadow-sm h-100")
validate_card = dbc.Card([
dbc.CardHeader(html.Span("VALIDATE (p.6)", style={"color": "#FFD54F"})),
dbc.CardBody([
html.P([
"Validation was tried with the July 21 poll in which ",
html.Strong("zero"),
" people took part in.",
], style={"fontSize": "0.85rem"}),
html.P(
"That points to one of two things: the question wasn't formulated to invite "
"a response, or this audience doesn't engage with the poll format at all.",
style={"fontSize": "0.85rem"}),
]),
], className="shadow-sm h-100")
analyze_extract_row = html.Div(
[audience_card, extract_card],
style={"display": "grid", "gridTemplateColumns": "1fr 1fr", "gap": "1rem", "marginBottom": "1rem"},
)
formulate_validate_row = html.Div(
[register_card, validate_card],
style={"display": "grid", "gridTemplateColumns": "4fr 1fr", "gap": "1rem", "marginBottom": "1rem"},
)
return dbc.Container([framework_card, analyze_extract_row, formulate_validate_row],
fluid=True, className="pb-3")
def _composition_count_table(counter, header):
_th_yellow = {"backgroundColor": "#fdf6d8"}
return dbc.Table([
html.Thead(html.Tr([html.Th(header, style=_th_yellow), html.Th("Count", style=_th_yellow)])),
html.Tbody([html.Tr([html.Td(k), html.Td(str(v))])
for k, v in (counter.most_common() if hasattr(counter, "most_common") else counter)]),
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
_NOTABLE_SENIORITIES = ("C-Level/Founder", "Director/Head/Principal")
def _audience_composition_findings(cohort, month_rows, period_label):
"""Turns the raw Sector/Function/Seniority/Country counts into three named
findings instead of leaving a reader to notice them in four separate tables —
computed fresh from whatever month is selected, not hardcoded prose, so it
stays true for every month rather than just describing July forever."""
total = cohort["total"]
top_sector, top_sector_n = cohort["sector"].most_common(1)[0]
top_country, top_country_n = cohort["country"][0]
notable_this_month = [d for d in month_rows if d.get("seniority") in _NOTABLE_SENIORITIES]
bullets = [
f"{top_sector_n} of {total} new followers this month are {top_sector} — the "
f"largest single group.",
f"{top_country_n} of {total} are based in {top_country}"
+ (f" ({top_country_n / total * 100:.0f}%)." if total else "."),
]
if notable_this_month:
names = ", ".join(f"{d.get('name', '')} ({d.get('company', '') or d.get('seniority', '')})"
for d in notable_this_month)
bullets.append(
f"{len(notable_this_month)} notable (C-Level/Founder or Director/Head/Principal) "
f"new follower{'s' if len(notable_this_month) != 1 else ''} this month: {names}.")
return dbc.Card([
dbc.CardHeader("What This Month's Followers Tell Us"),
dbc.CardBody(html.Ul([html.Li(b, style={"marginBottom": "4px"}) for b in bullets],
style={"fontSize": "0.85rem", "paddingLeft": "1.1rem", "marginBottom": "0"})),
], className="mb-3 shadow-sm")
def _audience_composition_insights_page(month_value="2026-07"):
"""Sector/Function/Seniority/Country, all four together, for the selected
month's cohort specifically — the full demographic/firmographic profile of who
joined that month, proving each entry was individually researched and
classified, not auto-tagged. Follower Study carries the same four dimensions at
full historical scale plus a Sector x Seniority cross-tab."""
period_label = report._period_label(month_value)
cohort = follower_directory_data.composition_stats(DIRECTORY_DATA, month_value)
if not cohort["total"]:
return dbc.Container([
dbc.Alert(f"No {period_label} cohort profiled yet.", color="info"),
], fluid=True, className="pb-3")
month_rows = [d for d in DIRECTORY_DATA if d.get("followedSort") == month_value]
return dbc.Container([
_audience_composition_findings(cohort, month_rows, period_label),
dbc.Card([
dbc.CardHeader(f"{period_label}'s Followers, by Sector, Function, Seniority and Country"),
dbc.CardBody(dbc.Row([
dbc.Col(_composition_count_table(cohort["sector"], "Sector"), md=3),
dbc.Col(_composition_count_table(cohort["function"], "Function"), md=3),
dbc.Col(_composition_count_table(cohort["seniority"], "Seniority"), md=3),
dbc.Col(_composition_count_table(cohort["country"], "Country"), md=3),
])),
], className="mb-3 shadow-sm"),
], fluid=True, className="pb-3")
def _monthly_notes_insights_page(month_value="2026-07"):
"""Full detail behind the Monthly Notes card's one-line description — moved to its
own page so the Reports tab's Audience Composition / Monthly Notes row can stay as
compact side-by-side abstracts, same pattern as Core Message / Campaign Roadmap."""
period_label = report._period_label(month_value)
notes = monthly_report_notes.NOTES.get(month_value)
if not notes:
return dbc.Alert(f"Notes for {period_label} have not been written yet.", color="info")
return dbc.Container([
dbc.Card([
dbc.CardHeader(f"Monthly Notes — {period_label}"),
dbc.CardBody([
_narrative_section("Features / Fixes Shipped", notes.get("features_shipped", [])),
_narrative_section("Data Quality Issues Found and Fixed", notes.get("data_quality", [])),
_narrative_section("Follower Study / Directory Updates", notes.get("follower_study", [])),
_narrative_section("Outstanding", notes.get("outstanding", [])),
]),
], className="mb-3 shadow-sm"),
], fluid=True, className="pb-3")
def _sector_seniority_crosstab(rows):
"""Sector x Seniority pivot over the whole (pre-August) Directory — only
meaningful at this scale (102 people), which is why it lives here rather than
on the July-only Audience Composition page (n=22, too sparse to read)."""
from collections import Counter
sectors = Counter(r.get("sector", "Unknown") for r in rows).most_common(6)
seniorities = [s for s, _ in Counter(r.get("seniority", "Unknown") for r in rows).most_common(6)]
sector_names = [s for s, _ in sectors]
grid = {s: Counter() for s in sector_names}
for r in rows:
s = r.get("sector", "Unknown")
if s in grid:
grid[s][r.get("seniority", "Unknown")] += 1
_th_yellow = {"backgroundColor": "#fdf6d8"}
return dbc.Table([
html.Thead(html.Tr([html.Th("Sector \\ Seniority", style=_th_yellow)] +
[html.Th(sen, style={**_th_yellow, "fontSize": "0.68rem"}) for sen in seniorities])),
html.Tbody([
html.Tr([html.Td(s, style={"fontWeight": "600", "whiteSpace": "nowrap"})] +
[html.Td(grid[s].get(sen, 0) or "") for sen in seniorities])
for s in sector_names
]),
], bordered=True, size="sm", style={"fontSize": "0.78rem"})
_SENIOR_TIERS = ("C-Level/Founder", "Director/Head/Principal", "Senior IC/Lead")
_JUNIOR_TIERS = ("Student/Trainee",)
def _follower_study_findings(whole, non_team, period_label, month_rows=None, clusters=None):
"""Two dynamically-computed cumulative findings in the same spirit as
assets/follower_study.html's own hand-written ones ("bimodal seniority,"
"shallow-network audience") — but computed live from whatever's in the
Directory through the selected month, not hardcoded to July's specific
numbers, so the finding stays honest for any month instead of silently
describing a snapshot that's since moved on.
Plus, ahead of those two, what actually happened THIS month specifically —
a monthly report showing "112 followers to date" as its headline is
answering the wrong question; the reader wants to know what changed in the
period this report covers, with the cumulative picture as supporting
context, not the other way around."""
total = whole["total"]
if not total:
return html.Div()
senior_n = sum(whole["seniority"].get(t, 0) for t in _SENIOR_TIERS)
junior_n = sum(whole["seniority"].get(t, 0) for t in _JUNIOR_TIERS)
first_degree_n = sum(1 for d in non_team if d.get("connection") == "1st")
this_month_bullets = []
if month_rows:
new_notable = [d for d in month_rows if d.get("seniority") in _NOTABLE_SENIORITIES]
if new_notable:
names = ", ".join(f"{d.get('name', '')} ({d.get('company', '') or d.get('seniority', '')})"
for d in new_notable)
this_month_bullets.append(
f"{len(new_notable)} new notable contact{'s' if len(new_notable) != 1 else ''} "
f"joined this month: {names}.")
if clusters:
from collections import Counter
month_companies = Counter(d.get("company", "").strip() for d in month_rows
if d.get("company", "").strip())
grown = [f"{c} ({total_n} total, {month_companies[c]} new this month)"
for c, total_n in clusters if month_companies.get(c)]
if grown:
this_month_bullets.append("Account clusters that grew this month: "
+ "; ".join(grown) + ".")
this_month_bullets = [html.Li(html.Strong(b), style={"marginBottom": "4px"}) for b in this_month_bullets]
if not this_month_bullets:
this_month_bullets = [html.Li(html.Em("No new notable contacts or growing account "
"clusters this month."), style={"marginBottom": "4px"})]
overall_bullets = [
f"Overall, {senior_n} of {total} ({senior_n / total * 100:.0f}%) sit at C-Level/Founder, "
f"Director/Head/Principal, or Senior IC/Lead level, while {junior_n} "
f"({junior_n / total * 100:.0f}%) are students or trainees — both ends of the "
f"seniority range are represented, not just the middle.",
f"{first_degree_n} of {total} ({first_degree_n / total * 100:.0f}%) are 1st-degree "
f"connections — the rest found the Page through shares, group overlap, or "
f"algorithmic surfacing rather than a direct personal tie.",
]
return dbc.Card([
dbc.CardHeader(f"What Changed This Month vs. The Full Picture"),
dbc.CardBody([
html.Div(f"New this month ({period_label}):", style={"fontWeight": "700", "fontSize": "0.82rem",
"marginBottom": "4px"}),
html.Ul(this_month_bullets, style={"fontSize": "0.85rem", "paddingLeft": "1.1rem",
"marginBottom": "12px"}),
html.Div("The full picture, to date:", style={"fontWeight": "700", "fontSize": "0.82rem",
"color": "#6b7a8d", "marginBottom": "4px"}),
html.Ul([html.Li(b, style={"marginBottom": "4px"}) for b in overall_bullets],
style={"fontSize": "0.85rem", "paddingLeft": "1.1rem", "marginBottom": "0",
"color": "#6b7a8d"}),
]),
], className="mb-3 shadow-sm")
def _follower_study_insights_page(month_value="2026-07"):
"""A native Dash page for Follower Study, computed live from the Directory, rather
than re-narrating the hand-authored assets/follower_study.html here — that would
just create a second copy of the same numbers to keep in sync by hand (the exact
Study/Directory drift problem already known to happen). Scoped to entries followed
on or before the selected month, so picking July shows the July report's own
snapshot rather than a preview of later months already in the Directory.
This is the "depth of manual work" page: the same four dimensions as Audience
Composition but across everyone profiled to date, a Sector x Seniority
cross-tab only meaningful at this scale, account clusters (2+ followers from the
same company — a composition fact, not an outreach claim: LinkedIn's free tier
doesn't support in-app outreach, so that angle is deliberately not pursued here),
and named individual VIP-tier contacts."""
period_label = report._period_label(month_value)
directory_as_of_month = [d for d in DIRECTORY_DATA
if not d.get("followedSort") or d.get("followedSort") <= month_value]
non_team = [d for d in directory_as_of_month if not d.get("is_team")]
whole = follower_directory_data.composition_stats(non_team)
month_rows = [d for d in non_team if d.get("followedSort") == month_value]
from collections import Counter
companies = Counter(d.get("company", "").strip() for d in non_team if d.get("company", "").strip())
clusters = [(c, n) for c, n in companies.most_common() if n >= 2]
month_companies = Counter(d.get("company", "").strip() for d in month_rows
if d.get("company", "").strip())
# Followers column shows this month's contribution inline (e.g. "5 (+3 this
# month)") rather than a flat all-time count with no monthly story — the whole
# point of surfacing this in an End of Month report is seeing where this
# month's people actually landed, not repeating a near-identical cumulative
# table every month with a marginally bigger number.
cluster_table = dbc.Table([
html.Thead(html.Tr([html.Th("Company", style={"backgroundColor": "#fdf6d8"}),
html.Th("Followers", style={"backgroundColor": "#fdf6d8"})])),
html.Tbody([
html.Tr([
html.Td(c),
html.Td([str(n), html.Span(f" (+{month_companies[c]} this month)",
style={"color": "#2e7d32", "fontWeight": "600"})
if month_companies.get(c) else None]),
]) for c, n in clusters
]),
], bordered=True, size="sm", style={"fontSize": "0.8rem"})
notable = [d for d in non_team if d.get("seniority") in _NOTABLE_SENIORITIES]
notable.sort(key=lambda d: (d.get("seniority") != "C-Level/Founder", d.get("name", "")))
def _notable_row(d):
# Rows added this month are highlighted in place, in context with everyone
# else at that seniority level, rather than only being called out in a
# separate list. Bootstrap's table CSS sets its own background on |
# itself, which paints over a |