PraneshJs commited on
Commit
835b233
·
verified ·
1 Parent(s): 6150a2f

added new features removed admin access and reduced thread sleep time from 60 t0 30 sec

Browse files
Files changed (1) hide show
  1. app.py +171 -243
app.py CHANGED
@@ -6,7 +6,6 @@ from google.oauth2.credentials import Credentials
6
  from google_auth_oauthlib.flow import InstalledAppFlow
7
  import json
8
  import gradio as gr
9
- import urllib.parse
10
  import time
11
  from datetime import datetime
12
  from pytz import timezone
@@ -16,9 +15,18 @@ from dotenv import load_dotenv
16
  # Load environment variables
17
  load_dotenv()
18
 
 
 
 
19
  # Scopes (read-only)
20
  SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
21
 
 
 
 
 
 
 
22
  def authorize():
23
  creds = None
24
 
@@ -56,6 +64,45 @@ def authorize():
56
 
57
  return gspread.authorize(creds)
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  # Function to get data from a specific sheet
60
  def get_sheet_data(spreadsheet, gid, sheet_name):
61
  try:
@@ -118,7 +165,7 @@ def get_studentwise_data(spreadsheet):
118
  print(f"❌ Error loading Studentwise Reward Points: {str(e)}")
119
  return None
120
 
121
- # NEW FUNCTION: Load and cache reward points activity data
122
  def load_reward_points_data():
123
  """Load and cache reward points activity data"""
124
  try:
@@ -153,7 +200,7 @@ def load_reward_points_data():
153
  print(f"❌ Error loading Reward Points data: {str(e)}")
154
  return None
155
 
156
- # MODIFIED FUNCTION: Get activity details from cached data in breakdown format
157
  def get_activity_details(roll_no, reward_points_df):
158
  """Get activity details for a specific roll number from cached reward points data in breakdown format"""
159
  try:
@@ -400,7 +447,7 @@ def get_details_info(spreadsheet):
400
  print("🚀 Initializing application...")
401
  client = authorize()
402
 
403
- # 🔧 Get spreadsheet IDs from environment variables
404
  MAIN_SHEET_ID = os.getenv('GOOGLE_SHEET_ID') # Your main sheets (20 sheets)
405
  STUDENTWISE_SHEET_ID = os.getenv('STUDENTWISE_SHEET_ID') # Studentwise Reward Points sheet
406
 
@@ -436,12 +483,12 @@ sheet_configs = [
436
  {"gid": 400900059, "name": "Sheet_20"}
437
  ]
438
 
439
- # 🕒 GLOBAL DATA CACHE WITH 12-HOUR AUTO-REFRESH (NOW INCLUDES REWARD POINTS DATA)
440
  data_cache = {
441
  "combined_df": None,
442
  "studentwise_data": None,
443
  "details_info": None,
444
- "reward_points_df": None, # NEW: Cache for reward points data
445
  "last_update": None,
446
  "cache_duration_hours": 12, # 12 hours cache
447
  "is_loading": False
@@ -517,14 +564,14 @@ def load_all_data():
517
  # Load details info from main spreadsheet
518
  details_info = get_details_info(main_spreadsheet)
519
 
520
- # NEW: Load reward points activity data
521
  reward_points_df = load_reward_points_data()
522
 
523
  # Update cache
524
  data_cache["combined_df"] = combined_df
525
  data_cache["studentwise_data"] = studentwise_data
526
  data_cache["details_info"] = details_info
527
- data_cache["reward_points_df"] = reward_points_df # NEW: Cache reward points data
528
  data_cache["last_update"] = datetime.now()
529
 
530
  load_time = time.time() - start_time
@@ -573,38 +620,34 @@ def auto_refresh_worker():
573
  # If error, wait 1 hour before trying again
574
  time.sleep(3600)
575
 
576
- # Auto Refersh When Time changes is found
577
- # Replace the details_sheet_watcher function (lines 577-610)
578
-
579
  def details_sheet_watcher():
580
- """Background watcher: checks every 1 minute if 'POINTS LAST UPDATED' changed - FIXED TOKEN SPAM & THREADING"""
581
  last_seen_update = None
582
  consecutive_errors = 0
583
  max_errors = 3
584
-
585
- # Persistent connection variables to prevent token regeneration
586
  watcher_client = None
587
  watcher_spreadsheet = None
588
  last_connection_time = None
589
- connection_duration = 2700 # 45 minutes in seconds
590
-
591
- print("👀 Starting optimized details sheet watcher (checks every 1 minute)...")
592
-
 
 
593
  while True:
594
  try:
595
- # Skip if data is currently being loaded to prevent conflicts
596
  if data_cache["is_loading"]:
597
  print("⏳ Watcher: Skipping check - data loading in progress")
598
- time.sleep(60)
599
  continue
600
-
601
- # Create/refresh connection only when needed (every 45 minutes)
602
  current_time = datetime.now()
603
  if (watcher_client is None or
604
  watcher_spreadsheet is None or
605
  last_connection_time is None or
606
  (current_time - last_connection_time).total_seconds() > connection_duration):
607
-
608
  try:
609
  print("🔄 Watcher: Refreshing connection...")
610
  watcher_client = authorize()
@@ -616,24 +659,22 @@ def details_sheet_watcher():
616
  consecutive_errors += 1
617
  watcher_client = None
618
  watcher_spreadsheet = None
619
- time.sleep(60)
620
  continue
621
-
622
- # Use persistent connection to get details info
623
  try:
624
  details_info = get_details_info(watcher_spreadsheet)
625
  except Exception as sheet_error:
626
  print(f"⚠️ Watcher sheet error: {str(sheet_error)[:100]}...")
627
  consecutive_errors += 1
628
- # Force connection refresh on next iteration
629
  watcher_client = None
630
  watcher_spreadsheet = None
631
- time.sleep(60)
632
  continue
633
-
634
  if details_info and 'last_updated' in details_info:
635
  current_update = details_info['last_updated'].strip()
636
-
637
  if last_seen_update is None:
638
  last_seen_update = current_update
639
  print(f"🕒 Watcher: Monitoring established")
@@ -642,10 +683,8 @@ def details_sheet_watcher():
642
  print(f"🔄 CHANGE DETECTED!")
643
  print(f" Old: {last_seen_update[:60]}...")
644
  print(f" New: {current_update[:60]}...")
645
-
646
  last_seen_update = current_update
647
-
648
- # FIXED: Direct reload without creating new thread
649
  if not data_cache["is_loading"]:
650
  print("🔄 Reloading data directly...")
651
  try:
@@ -656,35 +695,29 @@ def details_sheet_watcher():
656
  else:
657
  print("⏳ Data already loading, skipping reload")
658
  else:
659
- # Only log status every 10 minutes to reduce spam
660
- if datetime.now().minute % 10 == 0:
661
- print(f"✅ Watcher: No changes detected ({datetime.now().strftime('%H:%M')})")
662
  else:
663
  print("⚠️ Watcher: Could not extract last_updated info")
664
  consecutive_errors += 1
665
-
666
- # Reset error counter on successful operation
667
  if details_info and consecutive_errors > 0:
668
  print(f"✅ Watcher: Connection restored (cleared {consecutive_errors} errors)")
669
  consecutive_errors = 0
670
-
671
  except Exception as e:
672
  consecutive_errors += 1
673
  print(f"⚠️ Watcher error #{consecutive_errors}: {str(e)[:100]}...")
674
-
675
- # Force connection refresh after errors
676
  watcher_client = None
677
  watcher_spreadsheet = None
678
-
679
- # Progressive backoff for multiple errors
680
  if consecutive_errors >= max_errors:
681
  error_wait = 300 # 5 minutes
682
  print(f"❌ Too many watcher errors, waiting {error_wait//60} minutes...")
683
  time.sleep(error_wait)
684
  consecutive_errors = 0
685
-
686
- # Wait 1 minute before next check
687
- time.sleep(60)
688
 
689
  def get_detailed_student_points(roll_no, studentwise_data):
690
  """Get detailed points breakdown from studentwise data"""
@@ -761,7 +794,7 @@ def get_detailed_student_points(roll_no, studentwise_data):
761
 
762
  def calculate_yearwise_average_points():
763
  """Calculate year-wise average points from the combined data"""
764
- combined_df, _, _, _ = get_cached_data() # FIX: Get all 4 return values
765
  if combined_df.empty:
766
  return "❌ No data available to calculate averages"
767
 
@@ -816,8 +849,6 @@ def calculate_yearwise_average_points():
816
  output.append("=" * 90)
817
  return "\n".join(output)
818
 
819
-
820
-
821
  # Load initial data
822
  print("📊 Loading initial data...")
823
  load_all_data()
@@ -827,7 +858,7 @@ refresh_thread = threading.Thread(target=auto_refresh_worker, daemon=True)
827
  refresh_thread.start()
828
  print("🕒 Auto-refresh thread started (updates every 12 hours)")
829
 
830
- # 1 min auto-refresh thread to watch details sheet changes
831
  watcher_thread = threading.Thread(target=details_sheet_watcher, daemon=True)
832
  watcher_thread.start()
833
  print("👀 Details sheet watcher started (checks every 1 minute)")
@@ -840,6 +871,9 @@ def search_student(roll_no):
840
  # Convert roll number to uppercase for consistent searching
841
  roll_no = roll_no.strip().upper()
842
 
 
 
 
843
  # Get cached data (fast response, auto-refreshes every 12 hours)
844
  combined_df, studentwise_data, details_info, reward_points_df = get_cached_data()
845
 
@@ -865,16 +899,15 @@ def search_student(roll_no):
865
  student_name = str(record.get('STUDENT NAME', 'Unknown')).strip()
866
  student_year = str(record.get('YEAR', '')).strip()
867
 
868
- # Time zone Conversion
869
- ist = timezone("Asia/Kolkata")
870
  now_ist = datetime.now(ist).strftime("%Y-%m-%d %H:%M:%S")
871
  # Log to see which roll number and student name is searched by user
872
  print(f"Roll No Searched: {roll_no} | Student Name: {student_name} | Time (IST): {now_ist}")
873
 
874
  # Format output - Simplified version
875
  output = []
 
876
  output.append("=" * 80)
877
- output.append("STUDENT DETAILS")
878
  output.append("=" * 80)
879
 
880
  # Main student details
@@ -927,7 +960,7 @@ def search_student(roll_no):
927
  output.append(" Keep up the great work! 🌟")
928
  output.append(" Refer Reward points Breakdown for more details")
929
 
930
- # MODIFIED: Add individual activity details from cached reward points data
931
  activity_details = get_activity_details(roll_no, reward_points_df)
932
  if activity_details:
933
  output.append(activity_details)
@@ -937,7 +970,6 @@ def search_student(roll_no):
937
  if detailed_points:
938
  output.append(detailed_points)
939
 
940
-
941
  # Add last updated info
942
  if details_info and 'last_updated' in details_info:
943
  output.append("\n" + "-" * 60)
@@ -967,14 +999,16 @@ def get_system_info():
967
  if not details_info:
968
  return "❌ No system information available"
969
 
 
 
 
970
  output = []
971
  output.append("=" * 80)
972
  output.append("SYSTEM INFORMATION")
973
  output.append("=" * 80)
974
-
975
  # Average Points
976
  if 'average_points' in details_info:
977
- output.append("\nAVERAGE REWARD POINTS BY YEAR:")
978
  output.append("-" * 40)
979
  for year, points in details_info['average_points'].items():
980
  if points:
@@ -987,21 +1021,21 @@ def get_system_info():
987
 
988
  # Redemption Dates
989
  if 'ip1_redemption' in details_info:
990
- output.append("\nIP 1 REDEMPTION DATES:")
991
  output.append("-" * 40)
992
  for semester, date in details_info['ip1_redemption'].items():
993
  if date and date != '-':
994
  output.append(f"{semester:<10}: {date}")
995
 
996
  if 'ip2_redemption' in details_info:
997
- output.append("\nIP 2 REDEMPTION DATES:")
998
  output.append("-" * 40)
999
  for semester, date in details_info['ip2_redemption'].items():
1000
  if date and date != '-':
1001
  output.append(f"{semester:<10}: {date}")
1002
 
1003
  if 'last_updated' in details_info:
1004
- output.append(f"\nLAST UPDATED:")
1005
  output.append("-" * 40)
1006
  output.append(details_info['last_updated'])
1007
 
@@ -1020,86 +1054,42 @@ def get_system_info():
1020
 
1021
  return "\n".join(output)
1022
 
1023
- # Admin UI Controls
1024
- def build_admin_section():
1025
- """Build admin controls section"""
1026
- with gr.Accordion("🔧 Admin Controls", open=False, visible=True) as admin_accordion:
1027
- admin_key = gr.Textbox(
1028
- label="Enter Admin Key",
1029
- type="password",
1030
- placeholder="Admin Only",
1031
- value=""
1032
- )
1033
- load_button = gr.Button("🔁 Reload All Data", visible=False, variant="primary")
1034
- admin_status = gr.Markdown("ℹ️ Enter admin key to access controls", visible=True)
1035
-
1036
- def verify_admin_key(key):
1037
- """Verify admin key and show/hide controls"""
1038
- if key.strip() == os.getenv("ADMIN_KEY", ""):
1039
- return (
1040
- gr.update(visible=True), # Show reload button
1041
- "✅ Access granted. You can reload data now."
1042
- )
1043
- elif key.strip() == "":
1044
- return (
1045
- gr.update(visible=False), # Hide reload button
1046
- "ℹ️ Enter admin key to access controls"
1047
- )
1048
- else:
1049
- return (
1050
- gr.update(visible=False), # Hide reload button
1051
- "❌ Invalid admin key."
1052
- )
1053
 
1054
- def admin_reload():
1055
- """Admin function to reload all data"""
1056
- try:
1057
- print("🔧 Admin reload triggered...")
1058
- combined_df, studentwise_data, details_info, reward_points_df = load_all_data()
1059
-
1060
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1061
- return f"✅ Data reloaded successfully at {timestamp}\n📊 Total records: {len(combined_df) if not combined_df.empty else 0}"
1062
- except Exception as e:
1063
- return f"❌ Error reloading data: {str(e)}"
1064
-
1065
- # Event handlers
1066
- admin_key.change(
1067
- fn=verify_admin_key,
1068
- inputs=admin_key,
1069
- outputs=[load_button, admin_status]
1070
- )
1071
-
1072
- load_button.click(
1073
- fn=admin_reload,
1074
- outputs=admin_status
1075
- )
1076
 
1077
- return admin_accordion, admin_key, load_button, admin_status
1078
-
1079
- # Function to determine if admin mode is enabled via URL parameter
1080
- def check_admin_mode(request: gr.Request) -> bool:
1081
- """Check if admin mode is enabled via URL parameter"""
1082
- try:
1083
- query_params = urllib.parse.parse_qs(str(request.url).split('?')[1] if '?' in str(request.url) else "")
1084
- admin_param = query_params.get("admin", [""])[0]
1085
- admin_mode_key = os.getenv("ADMIN_MODE_KEY", "")
1086
 
1087
- is_admin = admin_param == admin_mode_key and admin_mode_key != ""
1088
- if is_admin:
1089
- print(f"🔧 Admin mode activated via URL parameter")
1090
- return is_admin
1091
- except Exception as e:
1092
- print(f"⚠️ Error checking admin mode: {str(e)}")
1093
- return False
1094
-
1095
- # Create Gradio interface
1096
- with gr.Blocks(title="Student Reward Points Check", theme=gr.themes.Soft()) as app:
1097
- gr.Markdown("# 🎓 Student Reward Points Check")
1098
- gr.Markdown("### Search for student details including reward points and redemption dates")
1099
- gr.Markdown("🕒 **Auto-Updates**: Data automatically refreshes when there is a change in Reward Points Sheet")
1100
- gr.Markdown("### Fill this form for any issues/Feedback: [Issue/Feedback Form](https://docs.google.com/forms/d/e/1FAIpQLScnl0udcN2pUDENHl45HIj5HZbvDuwZ0g2eepBbp8tJYg-NvQ/viewform)")
1101
- # Store admin components for conditional visibility
1102
- admin_components = gr.State(None)
1103
 
1104
  with gr.Tabs():
1105
  with gr.TabItem("🔍 Student Search"):
@@ -1118,7 +1108,8 @@ with gr.Blocks(title="Student Reward Points Check", theme=gr.themes.Soft()) as a
1118
  lines=50,
1119
  max_lines=60,
1120
  show_copy_button=True,
1121
- autoscroll=False
 
1122
  )
1123
 
1124
  with gr.TabItem("ℹ️ System Information"):
@@ -1130,90 +1121,44 @@ with gr.Blocks(title="Student Reward Points Check", theme=gr.themes.Soft()) as a
1130
  show_copy_button=True,
1131
  autoscroll=False,
1132
  interactive=False,
1133
- show_label=True
1134
- )
1135
-
1136
- # NEW: Admin Controls as a separate tab
1137
- with gr.TabItem("🔧 Admin Controls", visible=False) as admin_tab: # Start hidden by default
1138
- gr.Markdown("### 🔐 Administrative Functions")
1139
- gr.Markdown("⚠️ **Access restricted to authorized personnel only**")
1140
-
1141
- with gr.Row():
1142
- with gr.Column(scale=1):
1143
- admin_key = gr.Textbox(
1144
- label="Enter Admin Key",
1145
- type="password",
1146
- placeholder="Enter admin password",
1147
- value=""
1148
- )
1149
-
1150
- with gr.Column(scale=1):
1151
- load_button = gr.Button("🔁 Reload All Data", visible=False, variant="primary", size="lg")
1152
-
1153
- admin_status = gr.Markdown("ℹ️ Enter admin key to access controls", visible=True)
1154
-
1155
- # Admin functions
1156
- def verify_admin_key(key):
1157
- """Verify admin key and show/hide controls"""
1158
- if key.strip() == os.getenv("ADMIN_KEY", ""):
1159
- return (
1160
- gr.update(visible=True), # Show reload button
1161
- "✅ **Access Granted!** You can now reload data."
1162
- )
1163
- elif key.strip() == "":
1164
- return (
1165
- gr.update(visible=False), # Hide reload button
1166
- "ℹ️ Enter admin key to access controls"
1167
- )
1168
- else:
1169
- return (
1170
- gr.update(visible=False), # Hide reload button
1171
- "❌ **Access Denied!** Invalid admin key."
1172
- )
1173
-
1174
- def admin_reload():
1175
- """Admin function to reload all data"""
1176
- try:
1177
- print("🔧 Admin reload triggered...")
1178
- combined_df, studentwise_data, details_info, reward_points_df = load_all_data()
1179
-
1180
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1181
- total_records = len(combined_df) if not combined_df.empty else 0
1182
-
1183
- return f"""✅ **Data Reload Successful!**
1184
-
1185
- 📅 **Timestamp:** {timestamp}
1186
- 📊 **Total Records:** {total_records:,}
1187
- 🔄 **Status:** All data sources refreshed
1188
- ⏰ **Next Auto-refresh:** 12 hours from now
1189
-
1190
- 🎯 **Data Sources Updated:**
1191
- • Main spreadsheet ({len(sheet_configs)} sheets)
1192
- • Studentwise reward points data
1193
- • Activity breakdown data
1194
- • System information"""
1195
-
1196
- except Exception as e:
1197
- return f"❌ **Error reloading data:** {str(e)}"
1198
-
1199
- # Event handlers for admin tab
1200
- admin_key.change(
1201
- fn=verify_admin_key,
1202
- inputs=admin_key,
1203
- outputs=[load_button, admin_status]
1204
- )
1205
-
1206
- load_button.click(
1207
- fn=admin_reload,
1208
- outputs=admin_status
1209
  )
1210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
  # Event handlers
1212
- search_btn.click(fn=search_student, inputs=roll_input, outputs=result_output)
1213
- roll_input.submit(fn=search_student, inputs=roll_input, outputs=result_output)
 
 
 
 
 
 
 
 
1214
  system_btn.click(fn=get_system_info, outputs=system_output)
1215
 
1216
- # 🌟 FOOTER SECTION
1217
  gr.Markdown("---")
1218
  with gr.Row():
1219
  with gr.Column():
@@ -1242,39 +1187,22 @@ with gr.Blocks(title="Student Reward Points Check", theme=gr.themes.Soft()) as a
1242
  elem_id="footer"
1243
  )
1244
 
1245
- # FIXED: Admin mode handler on app load (for tab visibility) - SIMPLIFIED
1246
- def setup_admin_mode(request: gr.Request):
1247
- """Setup admin mode based on URL parameters"""
1248
- try:
1249
- is_admin = check_admin_mode(request)
1250
- if is_admin:
1251
- print("🔧 Admin tab will be visible")
1252
- return gr.update(visible=True) # Show admin tab
1253
- else:
1254
- return gr.update(visible=False) # Hide admin tab
1255
- except Exception as e:
1256
- print(f"⚠️ Error in setup_admin_mode: {str(e)}")
1257
- return gr.update(visible=False) # Hide admin tab on error
1258
-
1259
- # FIXED: System info initialization function
1260
  def initialize_system_info():
1261
- """Initialize system information display"""
 
1262
  try:
1263
- return get_system_info()
 
 
1264
  except Exception as e:
1265
  print(f"⚠️ Error initializing system info: {str(e)}")
1266
- return "⚠️ System information will be available after data loads completely."
1267
-
1268
- # Apply admin mode check on load (only affects admin tab visibility)
1269
- app.load(
1270
- fn=setup_admin_mode,
1271
- outputs=admin_tab
1272
- )
1273
 
1274
- # Load system info on startup (separate from admin mode)
1275
  app.load(
1276
  fn=initialize_system_info,
1277
- outputs=system_output
1278
  )
1279
 
1280
  # Launch the app
 
6
  from google_auth_oauthlib.flow import InstalledAppFlow
7
  import json
8
  import gradio as gr
 
9
  import time
10
  from datetime import datetime
11
  from pytz import timezone
 
15
  # Load environment variables
16
  load_dotenv()
17
 
18
+ # Time zone Conversion
19
+ ist = timezone("Asia/Kolkata")
20
+
21
  # Scopes (read-only)
22
  SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
23
 
24
+ # GLOBAL ANALYTICS VARIABLES - Add at the top after imports
25
+ visit_count = 0
26
+ searched_roll_numbers = []
27
+ unique_searches = set()
28
+ search_history = [] # Store with timestamps
29
+
30
  def authorize():
31
  creds = None
32
 
 
64
 
65
  return gspread.authorize(creds)
66
 
67
+ # Function to increment visit count
68
+ def increment_visit_count():
69
+ global visit_count
70
+ visit_count += 1
71
+
72
+ # Function to log searched roll numbers
73
+ def log_search(roll_no):
74
+ global searched_roll_numbers, unique_searches, search_history
75
+
76
+ if roll_no and roll_no.strip():
77
+ roll_no = roll_no.strip().upper()
78
+ searched_roll_numbers.append(roll_no)
79
+ unique_searches.add(roll_no)
80
+
81
+ # Add to search history with timestamp
82
+ search_entry = {
83
+ 'roll_no': roll_no,
84
+ 'timestamp': datetime.now(ist).strftime('%Y-%m-%d %H:%M:%S'),
85
+ 'search_count': len(searched_roll_numbers)
86
+ }
87
+ search_history.append(search_entry)
88
+
89
+ # Function to get analytics data
90
+ def get_analytics_summary():
91
+ """Get current analytics summary"""
92
+ total_searches = len(searched_roll_numbers)
93
+ unique_users = len(unique_searches)
94
+
95
+ # Get recent searches (last 5)
96
+ recent_searches = searched_roll_numbers[-5:] if searched_roll_numbers else []
97
+
98
+ return {
99
+ 'visits': visit_count,
100
+ 'total_searches': total_searches,
101
+ 'unique_searches': unique_users,
102
+ 'recent_searches': recent_searches,
103
+ 'all_searches': searched_roll_numbers
104
+ }
105
+
106
  # Function to get data from a specific sheet
107
  def get_sheet_data(spreadsheet, gid, sheet_name):
108
  try:
 
165
  print(f"❌ Error loading Studentwise Reward Points: {str(e)}")
166
  return None
167
 
168
+ # Function to load and cache reward points activity data
169
  def load_reward_points_data():
170
  """Load and cache reward points activity data"""
171
  try:
 
200
  print(f"❌ Error loading Reward Points data: {str(e)}")
201
  return None
202
 
203
+ # Function to get activity details from cached data in breakdown format
204
  def get_activity_details(roll_no, reward_points_df):
205
  """Get activity details for a specific roll number from cached reward points data in breakdown format"""
206
  try:
 
447
  print("🚀 Initializing application...")
448
  client = authorize()
449
 
450
+ # Get spreadsheet IDs from environment variables
451
  MAIN_SHEET_ID = os.getenv('GOOGLE_SHEET_ID') # Your main sheets (20 sheets)
452
  STUDENTWISE_SHEET_ID = os.getenv('STUDENTWISE_SHEET_ID') # Studentwise Reward Points sheet
453
 
 
483
  {"gid": 400900059, "name": "Sheet_20"}
484
  ]
485
 
486
+ # GLOBAL DATA CACHE WITH 12-HOUR AUTO-REFRESH
487
  data_cache = {
488
  "combined_df": None,
489
  "studentwise_data": None,
490
  "details_info": None,
491
+ "reward_points_df": None,
492
  "last_update": None,
493
  "cache_duration_hours": 12, # 12 hours cache
494
  "is_loading": False
 
564
  # Load details info from main spreadsheet
565
  details_info = get_details_info(main_spreadsheet)
566
 
567
+ # Load reward points activity data
568
  reward_points_df = load_reward_points_data()
569
 
570
  # Update cache
571
  data_cache["combined_df"] = combined_df
572
  data_cache["studentwise_data"] = studentwise_data
573
  data_cache["details_info"] = details_info
574
+ data_cache["reward_points_df"] = reward_points_df
575
  data_cache["last_update"] = datetime.now()
576
 
577
  load_time = time.time() - start_time
 
620
  # If error, wait 1 hour before trying again
621
  time.sleep(3600)
622
 
 
 
 
623
  def details_sheet_watcher():
624
+ """Background watcher: checks every 30 seconds if 'POINTS LAST UPDATED' changed - optimized timing"""
625
  last_seen_update = None
626
  consecutive_errors = 0
627
  max_errors = 3
628
+
 
629
  watcher_client = None
630
  watcher_spreadsheet = None
631
  last_connection_time = None
632
+ connection_duration = 2700 # 45 minutes
633
+
634
+ check_interval = 30 # seconds
635
+
636
+ print(f"👀 Starting optimized details sheet watcher (checks every {check_interval} seconds)...")
637
+
638
  while True:
639
  try:
 
640
  if data_cache["is_loading"]:
641
  print("⏳ Watcher: Skipping check - data loading in progress")
642
+ time.sleep(check_interval)
643
  continue
644
+
 
645
  current_time = datetime.now()
646
  if (watcher_client is None or
647
  watcher_spreadsheet is None or
648
  last_connection_time is None or
649
  (current_time - last_connection_time).total_seconds() > connection_duration):
650
+
651
  try:
652
  print("🔄 Watcher: Refreshing connection...")
653
  watcher_client = authorize()
 
659
  consecutive_errors += 1
660
  watcher_client = None
661
  watcher_spreadsheet = None
662
+ time.sleep(check_interval)
663
  continue
664
+
 
665
  try:
666
  details_info = get_details_info(watcher_spreadsheet)
667
  except Exception as sheet_error:
668
  print(f"⚠️ Watcher sheet error: {str(sheet_error)[:100]}...")
669
  consecutive_errors += 1
 
670
  watcher_client = None
671
  watcher_spreadsheet = None
672
+ time.sleep(check_interval)
673
  continue
674
+
675
  if details_info and 'last_updated' in details_info:
676
  current_update = details_info['last_updated'].strip()
677
+
678
  if last_seen_update is None:
679
  last_seen_update = current_update
680
  print(f"🕒 Watcher: Monitoring established")
 
683
  print(f"🔄 CHANGE DETECTED!")
684
  print(f" Old: {last_seen_update[:60]}...")
685
  print(f" New: {current_update[:60]}...")
 
686
  last_seen_update = current_update
687
+
 
688
  if not data_cache["is_loading"]:
689
  print("🔄 Reloading data directly...")
690
  try:
 
695
  else:
696
  print("⏳ Data already loading, skipping reload")
697
  else:
698
+ # Show heartbeat every 5 minutes instead of 10
699
+ if datetime.now().minute % 5 == 0 and datetime.now(ist).second < check_interval:
700
+ print(f"✅ Watcher: No changes detected ({datetime.now(ist).strftime('%H:%M')})")
701
  else:
702
  print("⚠️ Watcher: Could not extract last_updated info")
703
  consecutive_errors += 1
704
+
 
705
  if details_info and consecutive_errors > 0:
706
  print(f"✅ Watcher: Connection restored (cleared {consecutive_errors} errors)")
707
  consecutive_errors = 0
708
+
709
  except Exception as e:
710
  consecutive_errors += 1
711
  print(f"⚠️ Watcher error #{consecutive_errors}: {str(e)[:100]}...")
 
 
712
  watcher_client = None
713
  watcher_spreadsheet = None
 
 
714
  if consecutive_errors >= max_errors:
715
  error_wait = 300 # 5 minutes
716
  print(f"❌ Too many watcher errors, waiting {error_wait//60} minutes...")
717
  time.sleep(error_wait)
718
  consecutive_errors = 0
719
+
720
+ time.sleep(check_interval)
 
721
 
722
  def get_detailed_student_points(roll_no, studentwise_data):
723
  """Get detailed points breakdown from studentwise data"""
 
794
 
795
  def calculate_yearwise_average_points():
796
  """Calculate year-wise average points from the combined data"""
797
+ combined_df, _, _, _ = get_cached_data()
798
  if combined_df.empty:
799
  return "❌ No data available to calculate averages"
800
 
 
849
  output.append("=" * 90)
850
  return "\n".join(output)
851
 
 
 
852
  # Load initial data
853
  print("📊 Loading initial data...")
854
  load_all_data()
 
858
  refresh_thread.start()
859
  print("🕒 Auto-refresh thread started (updates every 12 hours)")
860
 
861
+ # Start details sheet watcher thread
862
  watcher_thread = threading.Thread(target=details_sheet_watcher, daemon=True)
863
  watcher_thread.start()
864
  print("👀 Details sheet watcher started (checks every 1 minute)")
 
871
  # Convert roll number to uppercase for consistent searching
872
  roll_no = roll_no.strip().upper()
873
 
874
+ # Log the search
875
+ log_search(roll_no)
876
+
877
  # Get cached data (fast response, auto-refreshes every 12 hours)
878
  combined_df, studentwise_data, details_info, reward_points_df = get_cached_data()
879
 
 
899
  student_name = str(record.get('STUDENT NAME', 'Unknown')).strip()
900
  student_year = str(record.get('YEAR', '')).strip()
901
 
 
 
902
  now_ist = datetime.now(ist).strftime("%Y-%m-%d %H:%M:%S")
903
  # Log to see which roll number and student name is searched by user
904
  print(f"Roll No Searched: {roll_no} | Student Name: {student_name} | Time (IST): {now_ist}")
905
 
906
  # Format output - Simplified version
907
  output = []
908
+ output.append(f"Hello {student_name} 👋")
909
  output.append("=" * 80)
910
+ output.append("YOUR DETAILS")
911
  output.append("=" * 80)
912
 
913
  # Main student details
 
960
  output.append(" Keep up the great work! 🌟")
961
  output.append(" Refer Reward points Breakdown for more details")
962
 
963
+ # Add individual activity details from cached reward points data
964
  activity_details = get_activity_details(roll_no, reward_points_df)
965
  if activity_details:
966
  output.append(activity_details)
 
970
  if detailed_points:
971
  output.append(detailed_points)
972
 
 
973
  # Add last updated info
974
  if details_info and 'last_updated' in details_info:
975
  output.append("\n" + "-" * 60)
 
999
  if not details_info:
1000
  return "❌ No system information available"
1001
 
1002
+ # Get analytics data
1003
+ analytics = get_analytics_summary()
1004
+
1005
  output = []
1006
  output.append("=" * 80)
1007
  output.append("SYSTEM INFORMATION")
1008
  output.append("=" * 80)
 
1009
  # Average Points
1010
  if 'average_points' in details_info:
1011
+ output.append("\n🎯 AVERAGE REWARD POINTS BY YEAR:")
1012
  output.append("-" * 40)
1013
  for year, points in details_info['average_points'].items():
1014
  if points:
 
1021
 
1022
  # Redemption Dates
1023
  if 'ip1_redemption' in details_info:
1024
+ output.append("\n📅 IP 1 REDEMPTION DATES:")
1025
  output.append("-" * 40)
1026
  for semester, date in details_info['ip1_redemption'].items():
1027
  if date and date != '-':
1028
  output.append(f"{semester:<10}: {date}")
1029
 
1030
  if 'ip2_redemption' in details_info:
1031
+ output.append("\n📅 IP 2 REDEMPTION DATES:")
1032
  output.append("-" * 40)
1033
  for semester, date in details_info['ip2_redemption'].items():
1034
  if date and date != '-':
1035
  output.append(f"{semester:<10}: {date}")
1036
 
1037
  if 'last_updated' in details_info:
1038
+ output.append(f"\n🕒 LAST UPDATED:")
1039
  output.append("-" * 40)
1040
  output.append(details_info['last_updated'])
1041
 
 
1054
 
1055
  return "\n".join(output)
1056
 
1057
+ # Function to get live analytics for header display
1058
+ def get_live_analytics():
1059
+ """Get current analytics for header display"""
1060
+ analytics = get_analytics_summary()
1061
+ return f"📊 Visits: {analytics['visits']} | 🔍 Searches: {analytics['total_searches']} | 👥 Unique Users: {analytics['unique_searches']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1062
 
1063
+ # Create Gradio interface with analytics
1064
+ with gr.Blocks(
1065
+ title="Student Reward Points Check",
1066
+ theme=gr.themes.Soft(),
1067
+ ) as app:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1068
 
1069
+ # Header with analytics
1070
+ with gr.Row():
1071
+ with gr.Column(scale=2):
1072
+ gr.Markdown("# 🎓 Student Reward Points Checker")
1073
+ gr.Markdown("### Search for Student details including Reward points with detailed information and Redemption dates")
1074
+ gr.Markdown("### எல்லா புகழும் இறைவனுக்கே ✝ 🕉 ☪")
1075
+ gr.Markdown("🕒 **Auto-Updates**: Data automatically refreshes when there is a change in Reward Points Sheet")
1076
+ gr.Markdown("### Fill this form for any Issue/Feedback: [Issue/Feedback Form](https://docs.google.com/forms/d/e/1FAIpQLScnl0udcN2pUDENHl45HIj5HZbvDuwZ0g2eepBbp8tJYg-NvQ/viewform)")
 
1077
 
1078
+ with gr.Column(scale=1):
1079
+ # Live analytics display
1080
+ analytics_display = gr.HTML(
1081
+ value=f"""
1082
+ <div class="analytics-display">
1083
+ <h2 style="margin: 0; color: black;">📊 Live Analytics</h2>
1084
+ <div style="margin: 10px 0;">
1085
+ <div>📊 Visits: {visit_count}</div>
1086
+ <div>🔍 Searches: {len(searched_roll_numbers)}</div>
1087
+ <div>👥 Unique Users: {len(unique_searches)}</div>
1088
+ </div>
1089
+ </div>
1090
+ """,
1091
+ elem_id="analytics_display"
1092
+ )
 
1093
 
1094
  with gr.Tabs():
1095
  with gr.TabItem("🔍 Student Search"):
 
1108
  lines=50,
1109
  max_lines=60,
1110
  show_copy_button=True,
1111
+ autoscroll=False,
1112
+ elem_id="result_output"
1113
  )
1114
 
1115
  with gr.TabItem("ℹ️ System Information"):
 
1121
  show_copy_button=True,
1122
  autoscroll=False,
1123
  interactive=False,
1124
+ show_label=True,
1125
+ elem_id="system_output"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126
  )
1127
 
1128
+ # Function to update analytics display
1129
+ def update_analytics_display():
1130
+ analytics = get_analytics_summary()
1131
+ return f"""
1132
+ <div class="analytics-display">
1133
+ <h2 style="margin: 0; color: black;">📊 Live Analytics</h3>
1134
+ <div style="margin: 10px 0;">
1135
+ <div>📊 Visits: {analytics['visits']}</div>
1136
+ <div>🔍 Searches: {analytics['total_searches']}</div>
1137
+ <div>👥 Unique Users: {analytics['unique_searches']}</div>
1138
+ </div>
1139
+ </div>
1140
+ """
1141
+
1142
+ # Enhanced search function that updates analytics
1143
+ def search_student_with_analytics(roll_no):
1144
+ result = search_student(roll_no)
1145
+ updated_analytics = update_analytics_display()
1146
+ return result, updated_analytics
1147
+
1148
  # Event handlers
1149
+ search_btn.click(
1150
+ fn=search_student_with_analytics,
1151
+ inputs=roll_input,
1152
+ outputs=[result_output, analytics_display]
1153
+ )
1154
+ roll_input.submit(
1155
+ fn=search_student_with_analytics,
1156
+ inputs=roll_input,
1157
+ outputs=[result_output, analytics_display]
1158
+ )
1159
  system_btn.click(fn=get_system_info, outputs=system_output)
1160
 
1161
+ # Footer section
1162
  gr.Markdown("---")
1163
  with gr.Row():
1164
  with gr.Column():
 
1187
  elem_id="footer"
1188
  )
1189
 
1190
+ # System info initialization function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
  def initialize_system_info():
1192
+ """Initialize system information display and increment visit count"""
1193
+ increment_visit_count()
1194
  try:
1195
+ result = get_system_info()
1196
+ updated_analytics = update_analytics_display()
1197
+ return result, updated_analytics
1198
  except Exception as e:
1199
  print(f"⚠️ Error initializing system info: {str(e)}")
1200
+ return "⚠️ System information will be available after data loads completely.", update_analytics_display()
 
 
 
 
 
 
1201
 
1202
+ # Load system info on startup and update analytics
1203
  app.load(
1204
  fn=initialize_system_info,
1205
+ outputs=[system_output, analytics_display]
1206
  )
1207
 
1208
  # Launch the app