GuglielmoTor commited on
Commit
67742c4
Β·
verified Β·
1 Parent(s): 2166780

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -82
app.py CHANGED
@@ -4,7 +4,8 @@ import json
4
  import os
5
  import logging
6
  import html
7
- import pandas as pd # Ensure pandas is imported if you're dealing with DataFrames
 
8
 
9
  # Import functions from your custom modules
10
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
@@ -15,7 +16,7 @@ from gradio_utils import get_url_user_token
15
  from Bubble_API_Calls import (
16
  fetch_linkedin_token_from_bubble,
17
  bulk_upload_to_bubble,
18
- fetch_linkedin_posts_data_from_bubble # Added new function
19
  )
20
  from Linkedin_Data_API_Calls import (
21
  fetch_linkedin_posts_core,
@@ -34,21 +35,20 @@ def check_token_status(token_state):
34
 
35
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
36
  """
37
- Processes the user token from the URL, fetches LinkedIn token from Bubble,
38
- fetches initial posts from Bubble, and updates the token state and UI accordingly.
39
- Returns updates for status_box, token_state, and sync_posts_to_bubble_btn.
40
  """
41
  logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
42
 
43
- # Initialize or copy existing state, adding bubble_posts_df
44
- new_state = token_state.copy() if token_state else {"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None}
45
- # Ensure org_urn is updated from input, and bubble_posts_df is reset/initialized for this run.
46
- # Token will be set later if fetched.
47
- new_state.update({"org_urn": org_urn, "bubble_posts_df": None, "token": new_state.get("token")})
48
 
49
- # Determine button properties - default to hidden and non-interactive
50
- button_visible = True
51
- button_interactive = True
52
 
53
  client_id = os.environ.get("Linkedin_client_id")
54
  if not client_id:
@@ -57,87 +57,131 @@ def process_and_store_bubble_token(url_user_token, org_urn, token_state):
57
  else:
58
  new_state["client_id"] = client_id
59
 
60
- # Attempt to fetch LinkedIn token from Bubble (related to LinkedIn API access)
61
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
62
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
63
  try:
64
  parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
65
  if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
66
- new_state["token"] = parsed_linkedin_token # Update token in new_state
67
  logging.info("βœ… LinkedIn Token successfully fetched from Bubble.")
68
  else:
69
- new_state["token"] = None # Explicitly set to None if fetch fails
70
  logging.warning(f"❌ Failed to fetch a valid LinkedIn token from Bubble. Response: {parsed_linkedin_token}")
71
  except Exception as e:
72
- new_state["token"] = None # Explicitly set to None on error
73
  logging.error(f"❌ Exception while fetching LinkedIn token from Bubble: {e}")
74
  else:
75
- new_state["token"] = None # Ensure token is None if no valid url_user_token
76
  logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
77
 
78
- # Fetch posts from Bubble using org_urn
79
  current_org_urn = new_state.get("org_urn")
 
80
  if current_org_urn:
81
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
82
  try:
83
- # Assuming fetch_linkedin_posts_data_from_bubble returns a Pandas DataFrame or None
84
- bubble_posts_df, error_message = fetch_linkedin_posts_data_from_bubble(current_org_urn, "LI_posts")
85
- new_state["bubble_posts_df"] = bubble_posts_df # Store DataFrame in state
86
- logging.info(f"df {len(bubble_posts_df)}")
87
-
88
- if bubble_posts_df is not None and not new_state["bubble_posts_df"].empty:
89
- logging.info(f"βœ… Successfully fetched {len(bubble_posts_df)} posts from Bubble. Sync button will be disabled.")
90
- button_visible = False
91
- button_interactive = False
92
  else:
93
- logging.info("ℹ️ No posts found in Bubble for this organization or DataFrame is empty. Sync button will get visible.")
94
- # button_visible and button_interactive remain False
95
  except Exception as e:
96
- logging.error(f"❌ Error fetching posts from Bubble: {e}")
97
- # button_visible and button_interactive remain False
98
  else:
99
  logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
100
- # button_visible and button_interactive remain False
101
-
102
- token_status_message = check_token_status(new_state) # Check based on potentially updated new_state["token"]
103
-
104
- # Log the determined visibility before creating the update object
105
- logging.info(f"Token processing complete. LinkedIn Token Status: {token_status_message}. Button visible: {button_visible}, Button interactive: {button_interactive}")
106
-
107
- # Create a gr.update object for the button
108
- button_component_update = gr.update(visible=button_visible, interactive=button_interactive)
109
-
110
- return token_status_message, new_state, button_component_update
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def guarded_fetch_posts(token_state):
113
  """
114
- Fetches LinkedIn posts, analyzes them, and uploads to Bubble.
115
- This function is guarded by token availability.
116
  """
117
  logging.info("Starting guarded_fetch_posts process.")
118
- if not token_state or not token_state.get("token"): # Checks for LinkedIn token
119
  logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
120
- return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available. Please ensure token is fetched via URL parameter.</p>"
121
 
122
  client_id = token_state.get("client_id")
123
- token_dict = token_state.get("token") # This is the LinkedIn token dict
124
  org_urn = token_state.get('org_urn')
 
125
 
126
  if not org_urn:
127
  logging.error("Organization URN (org_urn) not found in token_state for guarded_fetch_posts.")
128
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
129
  if not client_id or client_id == "ENV VAR MISSING":
130
  logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
131
- return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing (check .env file or environment variables).</p>"
132
 
133
- # Additional check: Ensure the button was meant to be clickable (i.e., Bubble posts were found)
134
- # This is an indirect check, as the button's clickability should prevent this if UI works as intended.
135
- # However, adding a check on bubble_posts_df might be redundant if the button is correctly managed.
136
- # For now, relying on the LinkedIn token check as the primary guard for this function.
 
 
 
 
137
 
138
  try:
139
- logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn} using LinkedIn API.")
140
- processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn)
141
 
142
  if not processed_raw_posts:
143
  logging.info("No posts found to process via LinkedIn API after step 1.")
@@ -163,32 +207,33 @@ def guarded_fetch_posts(token_state):
163
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
164
  bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
165
 
166
- logging.info("Successfully fetched from LinkedIn and uploaded posts and comments to Bubble.")
167
- return "<p style='color:green; text-align:center;'>βœ… Posts and comments from LinkedIn uploaded to Bubble.</p>"
 
168
 
169
  except ValueError as ve:
170
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
171
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
172
  except Exception as e:
173
- logging.exception("An unexpected error occurred in guarded_fetch_posts.") # Logs full traceback
174
- return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred while processing LinkedIn data. Please check logs.</p>"
175
 
176
  def guarded_fetch_dashboard(token_state):
177
- """Fetches and renders the dashboard if token is available."""
178
  if not token_state or not token_state.get("token"):
179
  return "❌ Access denied. No token available for dashboard."
180
- return "<p style='text-align: center; color: #555;'>Dashboard content would load here if implemented.</p>"
 
 
 
181
 
182
 
183
  def guarded_fetch_analytics(token_state):
184
- """Fetches and renders analytics if token is available."""
185
  if not token_state or not token_state.get("token"):
186
  return ("❌ Access denied. No token available for analytics.",
187
  None, None, None, None, None, None, None)
188
  return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
189
 
190
  def run_mentions_and_load(token_state):
191
- """Generates mentions dashboard if token is available."""
192
  if not token_state or not token_state.get("token"):
193
  return ("❌ Access denied. No token available for mentions.", None)
194
  return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
@@ -197,44 +242,50 @@ def run_mentions_and_load(token_state):
197
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
198
  title="LinkedIn Post Viewer & Analytics") as app:
199
 
200
- # Initialize state with the new field for Bubble DataFrame
201
- token_state = gr.State(value={"token": None, "client_id": None, "org_urn": None, "bubble_posts_df": None})
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
204
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
205
 
206
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
207
- status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
208
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
209
 
210
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
211
 
212
  with gr.Tabs():
213
  with gr.TabItem("1️⃣ Dashboard & Sync"):
214
- gr.Markdown("Fetch initial data from Bubble. If posts are found, you can choose to sync newer posts from LinkedIn.")
215
 
216
  sync_posts_to_bubble_btn = gr.Button(
217
- "πŸ”„ Fetch from LinkedIn, Analyze & Store to Bubble", # Updated label for clarity
218
  variant="primary",
219
- visible=False,
220
  interactive=False
221
  )
222
 
223
  dashboard_html_output = gr.HTML(
224
  "<p style='text-align: center; color: #555;'>System initializing... "
225
- "Checking for existing data in Bubble. The 'Fetch from LinkedIn...' button will activate if initial data is found.</p>"
226
  )
227
 
228
- # Combined trigger: process tokens and Bubble data once both URL params are potentially loaded.
229
- # Using .then() to chain after initial load.
230
- # The `process_and_store_bubble_token` will run when `org_urn_display` (which is an output of app.load)
231
- # receives its value.
232
  org_urn_display.change(
233
  fn=process_and_store_bubble_token,
234
  inputs=[url_user_token_display, org_urn_display, token_state],
235
- outputs=[status_box, token_state, sync_posts_to_bubble_btn]
236
  )
237
- # Fallback if url_user_token_display changes after org_urn_display (less likely but for robustness)
238
  url_user_token_display.change(
239
  fn=process_and_store_bubble_token,
240
  inputs=[url_user_token_display, org_urn_display, token_state],
@@ -242,11 +293,15 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
242
  )
243
 
244
  sync_posts_to_bubble_btn.click(
245
- fn=guarded_fetch_posts,
246
- inputs=[token_state],
247
  outputs=[dashboard_html_output]
 
 
 
 
248
  )
249
-
250
  with gr.TabItem("2️⃣ Analytics"):
251
  gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
252
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
@@ -281,10 +336,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
281
  outputs=[mentions_html, mentions_plot]
282
  )
283
 
284
- # This app.load updates the status_box based on the initial token_state.
285
- # The process_and_store_bubble_token function will provide a more definitive update soon after.
286
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
287
- # Timer to periodically update the LinkedIn token status display
288
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
289
 
290
 
 
4
  import os
5
  import logging
6
  import html
7
+ import pandas as pd # Ensure pandas is imported
8
+ from datetime import datetime # Used for pd.Timestamp
9
 
10
  # Import functions from your custom modules
11
  from Data_Fetching_and_Rendering import fetch_and_render_dashboard
 
16
  from Bubble_API_Calls import (
17
  fetch_linkedin_token_from_bubble,
18
  bulk_upload_to_bubble,
19
+ fetch_linkedin_posts_data_from_bubble
20
  )
21
  from Linkedin_Data_API_Calls import (
22
  fetch_linkedin_posts_core,
 
35
 
36
  def process_and_store_bubble_token(url_user_token, org_urn, token_state):
37
  """
38
+ Processes user token, fetches LinkedIn token, fetches Bubble posts,
39
+ and determines if an initial fetch or update is needed for LinkedIn posts.
40
+ Updates token state and UI for the sync button.
41
  """
42
  logging.info(f"Processing token with URL user token: '{url_user_token}', Org URN: '{org_urn}'")
43
 
44
+ new_state = token_state.copy() if token_state else {
45
+ "token": None, "client_id": None, "org_urn": None,
46
+ "bubble_posts_df": None, "fetch_count_for_api": 0
47
+ }
48
+ new_state.update({"org_urn": org_urn, "bubble_posts_df": new_state.get("bubble_posts_df"), "fetch_count_for_api": new_state.get("fetch_count_for_api", 0)})
49
 
50
+ # Default button update: hidden and non-interactive
51
+ button_update = gr.update(visible=False, interactive=False, value="πŸ”„ Sync LinkedIn Posts")
 
52
 
53
  client_id = os.environ.get("Linkedin_client_id")
54
  if not client_id:
 
57
  else:
58
  new_state["client_id"] = client_id
59
 
 
60
  if url_user_token and "not found" not in url_user_token and "Could not access" not in url_user_token:
61
  logging.info(f"Attempting to fetch LinkedIn token from Bubble with user token: {url_user_token}")
62
  try:
63
  parsed_linkedin_token = fetch_linkedin_token_from_bubble(url_user_token)
64
  if isinstance(parsed_linkedin_token, dict) and "access_token" in parsed_linkedin_token:
65
+ new_state["token"] = parsed_linkedin_token
66
  logging.info("βœ… LinkedIn Token successfully fetched from Bubble.")
67
  else:
68
+ new_state["token"] = None
69
  logging.warning(f"❌ Failed to fetch a valid LinkedIn token from Bubble. Response: {parsed_linkedin_token}")
70
  except Exception as e:
71
+ new_state["token"] = None
72
  logging.error(f"❌ Exception while fetching LinkedIn token from Bubble: {e}")
73
  else:
74
+ new_state["token"] = None
75
  logging.info("No valid URL user token provided for LinkedIn token fetch, or an error was indicated.")
76
 
77
+ # Fetch posts from Bubble
78
  current_org_urn = new_state.get("org_urn")
79
+ bubble_posts_df = None
80
  if current_org_urn:
81
  logging.info(f"Attempting to fetch posts from Bubble for org_urn: {current_org_urn}")
82
  try:
83
+ fetched_df, error_message = fetch_linkedin_posts_data_from_bubble(current_org_urn, "LI_posts")
84
+ if error_message:
85
+ logging.warning(f"Error reported by fetch_linkedin_posts_data_from_bubble: {error_message}. Treating as no data.")
 
 
 
 
 
 
86
  else:
87
+ bubble_posts_df = fetched_df
88
+ new_state["bubble_posts_df"] = bubble_posts_df
89
  except Exception as e:
90
+ logging.error(f"❌ Error fetching posts from Bubble: {e}. Treating as no data.")
91
+ new_state["bubble_posts_df"] = None # Ensure it's None on error
92
  else:
93
  logging.warning("Org URN not available in state. Cannot fetch posts from Bubble.")
94
+
95
+ # Logic for determining fetch/update based on bubble_posts_df
96
+ # DATE_COLUMN_NAME is now 'published_at' and contains ISO datetime strings.
97
+ DATE_COLUMN_NAME = 'published_at'
98
+ DEFAULT_INITIAL_FETCH_COUNT = 100 # Standard number of posts for initial fetch
99
+
100
+ if new_state["bubble_posts_df"] is None or new_state["bubble_posts_df"].empty:
101
+ logging.info(f"ℹ️ No posts found in Bubble or DataFrame is empty. Button to fetch initial {DEFAULT_INITIAL_FETCH_COUNT} posts will be visible.")
102
+ new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
103
+ button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} LinkedIn Posts", visible=True, interactive=True)
104
+ else:
105
+ try:
106
+ df_for_date_check = new_state["bubble_posts_df"].copy() # Use a copy to avoid SettingWithCopyWarning
107
+ if DATE_COLUMN_NAME not in df_for_date_check.columns:
108
+ logging.warning(f"Date column '{DATE_COLUMN_NAME}' not found in Bubble posts DataFrame. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
109
+ new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
110
+ button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Missing)", visible=True, interactive=True)
111
+ elif df_for_date_check[DATE_COLUMN_NAME].isnull().all():
112
+ logging.warning(f"Date column '{DATE_COLUMN_NAME}' contains all null values. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
113
+ new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
114
+ button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Column Empty)", visible=True, interactive=True)
115
+ else:
116
+ # Convert ISO datetime strings to datetime objects
117
+ df_for_date_check[DATE_COLUMN_NAME] = pd.to_datetime(df_for_date_check[DATE_COLUMN_NAME], errors='coerce', utc=True)
118
+ last_post_date_utc = df_for_date_check[DATE_COLUMN_NAME].dropna().max()
119
+
120
+ if pd.isna(last_post_date_utc):
121
+ logging.warning(f"No valid dates found in '{DATE_COLUMN_NAME}' after conversion. Assuming initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
122
+ new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
123
+ button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (No Valid Dates)", visible=True, interactive=True)
124
+ else:
125
+ today_utc = pd.Timestamp('now', tz='UTC').normalize()
126
+ last_post_date_utc_normalized = last_post_date_utc.normalize()
127
+
128
+ time_difference_days = (today_utc - last_post_date_utc_normalized).days
129
+ logging.info(f"Last post date (UTC, normalized): {last_post_date_utc_normalized}, Today (UTC, normalized): {today_utc}, Difference: {time_difference_days} days.")
130
+
131
+ if time_difference_days >= 7:
132
+ num_weeks = max(1, time_difference_days // 7)
133
+ fetch_count = num_weeks * 10
134
+ new_state['fetch_count_for_api'] = fetch_count
135
+ button_label = f"πŸ”„ Update Last {num_weeks} Week(s) (~{fetch_count} Posts)"
136
+ logging.info(f"Data is {time_difference_days} days old. Update needed for {num_weeks} weeks, ~{fetch_count} posts.")
137
+ button_update = gr.update(value=button_label, visible=True, interactive=True)
138
+ else:
139
+ logging.info(f"Data is fresh ({time_difference_days} days old). No update needed now.")
140
+ new_state['fetch_count_for_api'] = 0
141
+ button_update = gr.update(visible=False, interactive=False)
142
+ except Exception as e:
143
+ logging.error(f"Error processing dates from Bubble posts: {e}. Defaulting to initial fetch of {DEFAULT_INITIAL_FETCH_COUNT} posts.")
144
+ new_state['fetch_count_for_api'] = DEFAULT_INITIAL_FETCH_COUNT
145
+ button_update = gr.update(value=f"πŸ”„ Fetch Initial {DEFAULT_INITIAL_FETCH_COUNT} (Date Error)", visible=True, interactive=True)
146
+
147
+ token_status_message = check_token_status(new_state)
148
+ logging.info(f"Token processing complete. LinkedIn Token Status: {token_status_message}. Button update: {button_update}. Fetch count for API: {new_state['fetch_count_for_api']}")
149
+ return token_status_message, new_state, button_update
150
 
151
  def guarded_fetch_posts(token_state):
152
  """
153
+ Fetches LinkedIn posts based on 'fetch_count_for_api' in token_state,
154
+ analyzes them, and uploads to Bubble.
155
  """
156
  logging.info("Starting guarded_fetch_posts process.")
157
+ if not token_state or not token_state.get("token"):
158
  logging.error("Access denied for guarded_fetch_posts. No LinkedIn token available.")
159
+ return "<p style='color:red; text-align:center;'>❌ Access denied. LinkedIn token not available.</p>"
160
 
161
  client_id = token_state.get("client_id")
162
+ token_dict = token_state.get("token")
163
  org_urn = token_state.get('org_urn')
164
+ fetch_count_value = token_state.get('fetch_count_for_api')
165
 
166
  if not org_urn:
167
  logging.error("Organization URN (org_urn) not found in token_state for guarded_fetch_posts.")
168
  return "<p style='color:red; text-align:center;'>❌ Configuration error: Organization URN missing.</p>"
169
  if not client_id or client_id == "ENV VAR MISSING":
170
  logging.error("Client ID not found or missing in token_state for guarded_fetch_posts.")
171
+ return "<p style='color:red; text-align:center;'>❌ Configuration error: LinkedIn Client ID missing.</p>"
172
 
173
+ if fetch_count_value == 0:
174
+ logging.info("guarded_fetch_posts called, but fetch_count_for_api is 0. Data is fresh.")
175
+ return "<p style='color:green; text-align:center;'>βœ… Data is already up-to-date. No new posts fetched.</p>"
176
+
177
+ if fetch_count_value is None: # Should ideally not happen with new logic, but as a safeguard
178
+ logging.warning("fetch_count_for_api is None in guarded_fetch_posts. This might indicate an issue. Defaulting to fetching a standard amount if your API supports it or all.")
179
+ # Depending on your API, None might mean fetch all or a default.
180
+ # If your API requires a specific count for "all", you might need to adjust here or in fetch_linkedin_posts_core.
181
 
182
  try:
183
+ logging.info(f"Step 1: Fetching core posts for org_urn: {org_urn}. Fetch count parameter for API: {fetch_count_value}")
184
+ processed_raw_posts, stats_map, _ = fetch_linkedin_posts_core(client_id, token_dict, org_urn, count=fetch_count_value)
185
 
186
  if not processed_raw_posts:
187
  logging.info("No posts found to process via LinkedIn API after step 1.")
 
207
  bulk_upload_to_bubble(li_post_stats, "LI_post_stats")
208
  bulk_upload_to_bubble(li_post_comments, "LI_post_comments")
209
 
210
+ action_performed = f"Initial data fetch (~{fetch_count_value} posts)" if fetch_count_value == DEFAULT_INITIAL_FETCH_COUNT else f"Data update (target: ~{fetch_count_value} posts)"
211
+ logging.info(f"Successfully completed: {action_performed}. Uploaded posts and comments to Bubble.")
212
+ return f"<p style='color:green; text-align:center;'>βœ… {action_performed} complete. Posts and comments from LinkedIn uploaded to Bubble.</p>"
213
 
214
  except ValueError as ve:
215
  logging.error(f"ValueError during LinkedIn data processing: {ve}")
216
  return f"<p style='color:red; text-align:center;'>❌ Error: {html.escape(str(ve))}</p>"
217
  except Exception as e:
218
+ logging.exception("An unexpected error occurred in guarded_fetch_posts.")
219
+ return "<p style='color:red; text-align:center;'>❌ An unexpected error occurred. Please check logs.</p>"
220
 
221
  def guarded_fetch_dashboard(token_state):
 
222
  if not token_state or not token_state.get("token"):
223
  return "❌ Access denied. No token available for dashboard."
224
+ if token_state.get("bubble_posts_df") is not None and not token_state["bubble_posts_df"].empty:
225
+ return f"<p style='text-align: center;'>Dashboard would show {len(token_state['bubble_posts_df'])} posts from Bubble.</p>"
226
+ else:
227
+ return "<p style='text-align: center; color: #555;'>No posts loaded from Bubble yet for the dashboard.</p>"
228
 
229
 
230
  def guarded_fetch_analytics(token_state):
 
231
  if not token_state or not token_state.get("token"):
232
  return ("❌ Access denied. No token available for analytics.",
233
  None, None, None, None, None, None, None)
234
  return fetch_and_render_analytics(token_state.get("client_id"), token_state.get("token"))
235
 
236
  def run_mentions_and_load(token_state):
 
237
  if not token_state or not token_state.get("token"):
238
  return ("❌ Access denied. No token available for mentions.", None)
239
  return generate_mentions_dashboard(token_state.get("client_id"), token_state.get("token"))
 
242
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
243
  title="LinkedIn Post Viewer & Analytics") as app:
244
 
245
+ # Define DEFAULT_INITIAL_FETCH_COUNT here if needed by guarded_fetch_posts for its messages,
246
+ # or ensure it's passed/accessible if logic depends on it there.
247
+ # For now, it's only used within process_and_store_bubble_token.
248
+ DEFAULT_INITIAL_FETCH_COUNT = 100
249
+
250
+
251
+ token_state = gr.State(value={
252
+ "token": None,
253
+ "client_id": None,
254
+ "org_urn": None,
255
+ "bubble_posts_df": None,
256
+ "fetch_count_for_api": 0
257
+ })
258
 
259
  gr.Markdown("# πŸš€ LinkedIn Organization Post Viewer & Analytics")
260
  gr.Markdown("Token is supplied via URL parameter for Bubble.io lookup. Then explore dashboard and analytics.")
261
 
262
  url_user_token_display = gr.Textbox(label="User Token (from URL - Hidden)", interactive=False, visible=False)
263
+ status_box = gr.Textbox(label="Overall LinkedIn Token Status", interactive=False, value="Initializing...")
264
  org_urn_display = gr.Textbox(label="Organization URN (from URL - Hidden)", interactive=False, visible=False)
265
 
266
  app.load(fn=get_url_user_token, inputs=None, outputs=[url_user_token_display, org_urn_display])
267
 
268
  with gr.Tabs():
269
  with gr.TabItem("1️⃣ Dashboard & Sync"):
270
+ gr.Markdown("System checks for existing data in Bubble. The button below will activate if new posts need to be fetched or updated from LinkedIn.")
271
 
272
  sync_posts_to_bubble_btn = gr.Button(
273
+ value="πŸ”„ Sync LinkedIn Posts",
274
  variant="primary",
275
+ visible=False,
276
  interactive=False
277
  )
278
 
279
  dashboard_html_output = gr.HTML(
280
  "<p style='text-align: center; color: #555;'>System initializing... "
281
+ "Checking for existing data in Bubble and LinkedIn token.</p>"
282
  )
283
 
 
 
 
 
284
  org_urn_display.change(
285
  fn=process_and_store_bubble_token,
286
  inputs=[url_user_token_display, org_urn_display, token_state],
287
+ outputs=[status_box, token_state, sync_posts_to_bubble_btn]
288
  )
 
289
  url_user_token_display.change(
290
  fn=process_and_store_bubble_token,
291
  inputs=[url_user_token_display, org_urn_display, token_state],
 
293
  )
294
 
295
  sync_posts_to_bubble_btn.click(
296
+ fn=guarded_fetch_posts,
297
+ inputs=[token_state],
298
  outputs=[dashboard_html_output]
299
+ ).then(
300
+ fn=process_and_store_bubble_token,
301
+ inputs=[url_user_token_display, org_urn_display, token_state],
302
+ outputs=[status_box, token_state, sync_posts_to_bubble_btn]
303
  )
304
+
305
  with gr.TabItem("2️⃣ Analytics"):
306
  gr.Markdown("View follower count and monthly gains for your organization (requires LinkedIn token).")
307
  fetch_analytics_btn = gr.Button("πŸ“ˆ Fetch Follower Analytics", variant="primary")
 
336
  outputs=[mentions_html, mentions_plot]
337
  )
338
 
 
 
339
  app.load(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
 
340
  gr.Timer(15.0).tick(fn=lambda ts: check_token_status(ts), inputs=[token_state], outputs=status_box)
341
 
342