Make posts table scrollable and fix file-finder to ignore non-export files

- 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.
This commit is contained in:
beantownbabe committed 2026-07-22 11:56:10 +02:00
1 parent 348fe65b88
commit f631ba26a0
2 files changed
+36 -8

No files matched your search

+1
View File
@@ -12,3 +12,4 @@ __pycache__/
# OS
.DS_Store
manual_followers_list_*
+35 -8
View File
@@ -103,9 +103,21 @@ def mark_posts(fig, fts_df, posts_df):
return fig
def _is_linkedin_export(fname, keyword, extensions):
"""True only for real LinkedIn export files (e.g. txproducts_followers_<id>.xls) —
excludes unrelated files that happen to contain the keyword (like a manually-built
roster) and Office lock files (~$...) left behind by a file currently open in Excel."""
lower = fname.lower()
if lower.startswith("~$") or lower.startswith("."):
return False
if not lower.startswith("txproducts_"):
return False
return keyword.lower() in lower and lower.endswith(extensions)
def _find_latest_file(keyword, extensions=(".xls", ".xlsx")):
"""Most recently modified file across DATA_DIR and UPLOAD_DIR whose name contains
`keyword` (case-insensitive) — replaces matching one exact hardcoded filename, so any
"""Most recently modified real LinkedIn export across DATA_DIR and UPLOAD_DIR whose
name contains `keyword` — replaces matching one exact hardcoded filename, so any
correctly-named LinkedIn export (including freshly uploaded ones) is picked up without
needing the file's exact auto-generated export ID."""
candidates = []
@@ -113,7 +125,7 @@ def _find_latest_file(keyword, extensions=(".xls", ".xlsx")):
if not os.path.isdir(folder):
continue
for fname in os.listdir(folder):
if keyword.lower() in fname.lower() and fname.lower().endswith(extensions):
if _is_linkedin_export(fname, keyword, extensions):
candidates.append(os.path.join(folder, fname))
if not candidates:
raise FileNotFoundError(
@@ -130,7 +142,7 @@ def _find_all_files(keyword, extensions=(".xls", ".xlsx")):
if not os.path.isdir(folder):
continue
for fname in os.listdir(folder):
if keyword.lower() in fname.lower() and fname.lower().endswith(extensions):
if _is_linkedin_export(fname, keyword, extensions):
candidates.append(os.path.join(folder, fname))
return sorted(set(candidates), key=os.path.getmtime)
@@ -494,14 +506,29 @@ def refresh_data():
id="posts-table",
data=_posts_table_df.to_dict("records"),
columns=[{"name": c, "id": c} for c in post_table_cols],
page_size=20,
style_table={"overflowX": "auto"},
page_action="none",
fixed_rows={"headers": True},
style_table={"overflowX": "auto", "overflowY": "auto", "maxHeight": "260px"},
style_header={"backgroundColor": "#f0f4f8", "color": "#333", "fontWeight": "bold", "border": "1px solid #ccc",
"fontSize": "11px", "padding": "3px 6px", "height": "22px", "lineHeight": "16px"},
"fontSize": "11px", "padding": "3px 6px", "height": "22px", "lineHeight": "16px",
"whiteSpace": "nowrap", "overflow": "hidden", "textOverflow": "ellipsis"},
style_cell={"backgroundColor": "#ffffff", "color": "#333", "border": "1px solid #e0e0e0",
"fontSize": "11px", "padding": "3px 6px", "height": "22px", "lineHeight": "16px",
"textAlign": "left",
"textAlign": "left", "whiteSpace": "nowrap",
"maxWidth": "300px", "overflow": "hidden", "textOverflow": "ellipsis"},
style_cell_conditional=[
{"if": {"column_id": "Short title"}, "minWidth": "250px", "maxWidth": "none"},
{"if": {"column_id": "Created date"}, "width": "95px", "minWidth": "95px", "maxWidth": "95px"},
{"if": {"column_id": "Post type"}, "width": "75px", "minWidth": "75px", "maxWidth": "75px"},
{"if": {"column_id": "Content Type"}, "width": "100px", "minWidth": "100px", "maxWidth": "100px"},
{"if": {"column_id": "Impressions"}, "width": "85px", "minWidth": "85px", "maxWidth": "85px"},
{"if": {"column_id": "Clicks"}, "width": "60px", "minWidth": "60px", "maxWidth": "60px"},
{"if": {"column_id": "Eng %"}, "width": "60px", "minWidth": "60px", "maxWidth": "60px"},
{"if": {"column_id": "CTR %"}, "width": "60px", "minWidth": "60px", "maxWidth": "60px"},
{"if": {"column_id": "Likes"}, "width": "60px", "minWidth": "60px", "maxWidth": "60px"},
{"if": {"column_id": "Comments"}, "width": "70px", "minWidth": "70px", "maxWidth": "70px"},
{"if": {"column_id": "Reposts"}, "width": "70px", "minWidth": "70px", "maxWidth": "70px"},
],
style_data_conditional=post_highlight_styles,
tooltip_data=[{c: {"value": str(row[c]), "type": "markdown"} for c in post_table_cols}
for row in _posts_table_df.to_dict("records")],