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:
1 parent
0ed6e6c457
commit
833cfc6388
2 files changed
+78
-2
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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"])
|
||||
Reference in new issue
Block a user