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>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-10-30 11:28:51 +01:00
1 parent 0ed6e6c457
commit 833cfc6388
2 files changed
+78 -2

No files matched your search

+36 -2
View File
@@ -18,6 +18,7 @@ import engagement_audit
import theme_tracker import theme_tracker
import conversation_tracker import conversation_tracker
import team_exports import team_exports
import visibility_tracker
# Overridable via env var so the same code runs unmodified in Docker/internal deployments — # Overridable via env var so the same code runs unmodified in Docker/internal deployments —
# defaults to this machine's path for local dev. # defaults to this machine's path for local dev.
@@ -2698,6 +2699,38 @@ def _team_kpi_row(tiles):
style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"}) style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"})
def _team_visibility_row(name):
"""Profile Appearances/Views - hand-logged (visibility_tracker.py), not parsed
from the Aggregate Analytics file, since LinkedIn has no export for either.
Shown regardless of whether the person's main export exists yet - visibility
logging and the Monday export are two independent data sources."""
entry = visibility_tracker.latest_for(name)
if not entry:
tiles = [
kpi("All Appearances", "—", "not logged yet"),
kpi("Search Appearances", "—", "not logged yet"),
kpi("Profile Views", "—", "not logged yet"),
]
else:
week_label = f"week of {entry['week_start']} – {entry['week_end']}"
def _pct_note(pct):
if pct is None:
return week_label
arrow = "▲" if pct > 0 else ("▼" if pct < 0 else "–")
return f"{arrow} {abs(pct)}% past 7 days · {week_label}"
tiles = [
kpi("All Appearances", f"{entry['all_appearances']:,}",
_pct_note(entry.get("all_appearances_change_pct"))),
kpi("Search Appearances", f"{entry['search_appearances']:,}",
_pct_note(entry.get("search_appearances_change_pct")), ORANGE),
kpi("Profile Views", f"{entry['profile_views']:,}" if entry.get("profile_views") is not None else "—",
week_label if entry.get("profile_views") is not None else "not logged yet", TEAL),
]
return section("Visibility (hand-logged, no LinkedIn export exists for this)", _team_kpi_row(tiles))
def _team_engagement_rate_stats(top_posts_df): def _team_engagement_rate_stats(top_posts_df):
"""(best_pct, avg_pct, post_count) - engagement rate per post is Engagements / """(best_pct, avg_pct, post_count) - engagement rate per post is Engagements /
Impressions, the same definition as the Page's own Avg. Eng. Rate tile. None for Impressions, the same definition as the Page's own Avg. Eng. Rate tile. None for
@@ -2943,10 +2976,11 @@ def _render_team_tab(name):
data lands. No code change needed when a person's first export arrives - data lands. No code change needed when a person's first export arrives -
team_exports.load_person_data() picks it up automatically by filename. The team_exports.load_person_data() picks it up automatically by filename. The
Download Report button is disabled whenever there's nothing real to send yet.""" Download Report button is disabled whenever there's nothing real to send yet."""
visibility_row = _team_visibility_row(name)
data = team_exports.load_person_data(name, DATA_DIR, UPLOAD_DIR) data = team_exports.load_person_data(name, DATA_DIR, UPLOAD_DIR)
if not data: if not data:
return _build_empty_person_dashboard(name), True return html.Div([visibility_row, _build_empty_person_dashboard(name)]), True
return _build_person_dashboard(name, data), False return html.Div([visibility_row, _build_person_dashboard(name, data)]), False
@app.callback( @app.callback(
+42
View File
@@ -0,0 +1,42 @@
"""Hand-logged Profile Views / Profile Appearances per team member, since LinkedIn
doesn't offer a file export for either (unlike the Aggregate Analytics content file
team_exports.py parses) - both only show live on a person's own profile page, under
"Profile appearances" and "Who's viewed your profile."
Profile Appearances = how often someone's name/headline showed up across LinkedIn
(search results, others' feeds, comments, People You May Know), split into All
appearances and the Search-appearances subset. Profile Views = actual visits to the
profile page. Add an entry here each Monday alongside that person's export - same
"add an entry, no code change needed" pattern as conversation_tracker.py.
"""
# {
# "name": "Mckinley Black",
# "week_start": "2026-10-20",
# "week_end": "2026-10-26",
# "all_appearances": 10,
# "all_appearances_change_pct": 67,
# "search_appearances": 1,
# "search_appearances_change_pct": 0,
# "profile_views": None,
# },
VISIBILITY_LOG = [
{
"name": "Mckinley Black",
"week_start": "2026-10-20",
"week_end": "2026-10-26",
"all_appearances": 10,
"all_appearances_change_pct": 67,
"search_appearances": 1,
"search_appearances_change_pct": 0,
"profile_views": None,
},
]
def latest_for(name):
"""Most recent logged week for a person, by week_end - None if nothing logged yet."""
entries = [e for e in VISIBILITY_LOG if e["name"] == name]
if not entries:
return None
return max(entries, key=lambda e: e["week_end"])