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 conversation_tracker
import team_exports
import visibility_tracker
# Overridable via env var so the same code runs unmodified in Docker/internal deployments —
# 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"})
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):
"""(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
@@ -2943,10 +2976,11 @@ def _render_team_tab(name):
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
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)
if not data:
return _build_empty_person_dashboard(name), True
return _build_person_dashboard(name, data), False
return html.Div([visibility_row, _build_empty_person_dashboard(name)]), True
return html.Div([visibility_row, _build_person_dashboard(name, data)]), False
@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"])