sonuprasad23 commited on
Commit
2a2b008
·
1 Parent(s): 34c8f66

Trying again

Browse files
Files changed (1) hide show
  1. server.py +97 -340
server.py CHANGED
@@ -1,490 +1,247 @@
1
- # app.py
2
-
3
  import eventlet
4
  eventlet.monkey_patch()
5
 
 
6
  import io
 
7
  import os
8
- import re
9
- import json
10
  import base64
11
- import threading
 
12
  from datetime import datetime
13
-
14
- import pandas as pd
15
  from flask import Flask, Response, request, jsonify
16
- from flask_cors import CORS
17
  from flask_socketio import SocketIO, emit
18
-
19
- # If QuantumBot is in worker.py as in the original repo
20
  from worker import QuantumBot
 
 
 
 
 
21
 
22
- # =========================
23
- # App & Socket Initialization
24
- # =========================
25
 
26
  app = Flask(__name__)
27
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
28
-
29
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
30
-
31
  # --- Definitive CORS Fix ---
32
- ALLOWED_ORIGINS = [
33
- os.getenv('FRONTEND_URL'),
34
- "http://localhost:3000",
35
- "http://127.0.0.1:5500",
36
- "null"
37
- ]
38
- ALLOWED_ORIGINS = [origin for origin in ALLOWED_ORIGINS if origin] # Filter out None values
39
 
40
  CORS(app, resources={r"/*": {"origins": ALLOWED_ORIGINS}})
41
  socketio = SocketIO(app, cors_allowed_origins=ALLOWED_ORIGINS, async_mode='eventlet')
42
 
43
- # Global state
44
  bot_instance = None
45
  session_data = {}
46
 
47
- # =========================
48
- # Utilities & Services
49
- # =========================
50
-
51
- def extract_patient_name(raw_name):
52
- """
53
- Extracts pure name (everything before 'DOB') from fields like 'NAME DOB: xx-xx-xxxx'
54
- """
55
- if not isinstance(raw_name, str):
56
- return ""
57
- # Remove everything after (and including) 'DOB'
58
- name_only = raw_name.split('DOB').strip()
59
- # Clean up leftover colon or numbers; remove trailing colon/spaces/numbers if present
60
- name_only = re.sub(r'[:\d\-\s]+$', '', name_only).strip()
61
- return name_only
62
-
63
- def normalize_str(v):
64
- """
65
- Safely convert any value (NaN/None/list/number/string) into a clean string.
66
- Prevents attribute errors when later trimming or matching.
67
- """
68
- try:
69
- import math
70
- # None or NaN -> empty
71
- if v is None:
72
- return ""
73
- if isinstance(v, float) and math.isnan(v):
74
- return ""
75
- # Join list-like values into a single string
76
- if isinstance(v, list):
77
- try:
78
- return " ".join("" if x is None else str(x).strip() for x in v).strip()
79
- except Exception:
80
- return str(v).strip()
81
- # Default string conversion + strip
82
- return str(v).strip()
83
- except Exception:
84
- # As a last resort, stringify
85
- return f"{v}".strip()
86
-
87
- def create_prn_lookup(df_app):
88
- """
89
- Build a lookup from pure patient name (extracted from 'Patient Name') to PRN.
90
- Both keys and values are normalized to robustly handle mixed cell types.
91
- """
92
- prn_dict = {}
93
- for _, row in df_app.iterrows():
94
- # Normalize inputs
95
- pn_raw = normalize_str(row.get('Patient Name', ''))
96
- prn_raw = normalize_str(row.get('PRN', ''))
97
- pure_name = extract_patient_name(pn_raw)
98
- if pure_name and prn_raw:
99
- prn_dict[pure_name] = prn_raw
100
- return prn_dict
101
-
102
  class GmailApiService:
103
  def __init__(self):
104
- self.sender_email = os.getenv('EMAIL_SENDER')
105
- self.service = None
106
  try:
107
- from google.oauth2 import service_account
108
- from googleapiclient.discovery import build
109
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
110
- if not base64_creds:
111
- print("[Server Log] WARNING: GDRIVE_SA_KEY_BASE64 not found.")
112
- return
113
- creds_json = base64.b64decode(base64_creds).decode('utf-8')
114
- creds_dict = json.loads(creds_json)
115
- credentials = service_account.Credentials.from_service_account_info(
116
- creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send']
117
- )
118
- if self.sender_email:
119
- credentials = credentials.with_subject(self.sender_email)
120
  self.service = build('gmail', 'v1', credentials=credentials)
121
  print("[Server Log] Gmail API Service initialized successfully")
122
- except Exception as e:
123
- print(f"[Server Log] Gmail API Error: {e}")
124
-
125
  def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
126
- status_color = "#28a745" if "completed" in status_text.lower() else "#ffc107" if "terminated" in status_text.lower() else "#dc3545"
127
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
128
  html_template = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title><style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }} .email-container {{ max-width: 700px; margin: 20px auto; background-color: #ffffff; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15); border-radius: 12px; overflow: hidden; }} .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }} .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }} .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }} .status-banner {{ background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; }} .content {{ padding: 40px 30px; }} .report-info {{ background: #fdfdff; border-left: 6px solid #4c00ff; padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }} .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }} .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }} .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }} .stat-card {{ background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); }} .stat-number {{ font-size: 36px; font-weight: 700; color: #4c00ff; margin-bottom: 10px; }} .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }} .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }} h3 {{ color: #8A0303; margin-bottom: 20px; font-size: 20px; font-weight: 600; }} </style></head><body> <div class="email-container"> <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div> <div class="status-banner">Process {status_text}</div> <div class="content"> <div class="report-info"> <h3>Report Summary</h3> <div class="info-grid"> <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div> <div class="info-item"><span><b>Process Type</b></span><span>{process_type}</span></div> <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div> <div class="info-item"><span><b>Status</b></span><span style="font-weight: bold; color: {status_color};">{status_text}</span></div> </div> </div> <h3>Processing Results</h3> <div class="stats-grid"> <div class="stat-card"><div class="stat-number">{stats['total']}</div><div>Total in File</div></div> <div class="stat-card"><div class="stat-number">{stats['processed']}</div><div>Attempted</div></div> <div class="stat-card"><div class="stat-number">{stats['successful']}</div><div>Done</div></div> <div class="stat-card"><div class="stat-number">{stats['bad']}</div><div>Bad State</div></div> <div class="stat-card"><div class="stat-number">{stats['skipped']}</div><div>Skipped</div></div> </div> <div class="attachments-section"> <h3>Attached Reports</h3> <p style="margin-bottom: 1rem;">The following files are attached and have been uploaded to Google Drive:</p> <ul style="list-style-position: inside; padding-left: 0;"> <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li> <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li> <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped.</li> </ul> </div> </div> <div class="footer"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div> </div> </body></html>"""
129
  return html_template
130
 
131
  def send_report(self, recipients, subject, body, attachments=None):
132
- if not self.service or not recipients:
133
- return False
134
  try:
135
- from email.mime.multipart import MIMEMultipart
136
- from email.mime.text import MIMEText
137
- from email.mime.base import MIMEBase
138
- from email import encoders
139
-
140
- message = MIMEMultipart()
141
- message['From'] = self.sender_email
142
- message['To'] = ', '.join(recipients)
143
- message['Subject'] = subject
144
  message.attach(MIMEText(body, 'html'))
145
-
146
  if attachments:
147
  for filename, content in attachments.items():
148
- part = MIMEBase('application', 'octet-stream')
149
- part.set_payload(content.encode('utf-8'))
150
- encoders.encode_base64(part)
151
- part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
152
  message.attach(part)
153
-
154
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
155
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
156
- print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}")
157
- return True
158
- except Exception as e:
159
- print(f"[Server Log] Failed to send email: {e}")
160
- return False
161
 
162
  class GoogleDriveService:
163
  def __init__(self):
164
- self.creds = None
165
- self.service = None
166
- self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
167
  try:
168
- from google.oauth2 import service_account
169
- from googleapiclient.discovery import build
170
-
171
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
172
- if not base64_creds or not self.folder_id:
173
- raise ValueError("Google Drive secrets not found.")
174
-
175
- creds_json = base64.b64decode(base64_creds).decode('utf-8')
176
- creds_dict = json.loads(creds_json)
177
- self.creds = service_account.Credentials.from_service_account_info(
178
- creds_dict, scopes=['https://www.googleapis.com/auth/drive']
179
- )
180
  self.service = build('drive', 'v3', credentials=self.creds)
181
  print("[Server Log] Google Drive Service initialized successfully.")
182
- except Exception as e:
183
- print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
184
-
185
  def upload_file(self, filename, file_content):
186
- if not self.service:
187
- return False
188
  try:
189
  from googleapiclient.http import MediaIoBaseUpload
190
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
191
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
192
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
193
- print(f"[Server Log] File '{filename}' uploaded to Google Drive.")
194
- return True
195
- except Exception as e:
196
- print(f"[Server Log] G-Drive ERROR: File upload failed: {e}")
197
- return False
198
 
199
  email_service = GmailApiService()
200
  drive_service = GoogleDriveService()
201
 
202
- # =========================
203
- # Core automation flow
204
- # =========================
 
205
 
206
  def run_automation_process(session_id):
207
  global bot_instance
208
- results = []
209
- is_terminated = False
210
- is_crash = False
211
  try:
212
- data = session_data.get(session_id, {})
213
- patient_data = data.get('patient_data')
214
- mode = data.get('mode')
215
-
216
- if not patient_data:
217
- raise ValueError("No patient data prepared for automation.")
218
-
219
  socketio.emit('initial_stats', {'total': len(patient_data)})
220
-
221
- results = bot_instance.start_processing(
222
- mode,
223
- patient_data,
224
- start_date=data.get('start_date'),
225
- end_date=data.get('end_date')
226
- )
227
  is_terminated = bot_instance.termination_event.is_set()
228
  except Exception as e:
229
- print(f"[Server Log] Fatal error in automation thread: {e}")
230
- is_crash = True
231
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
232
  finally:
233
  socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
234
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
235
- if bot_instance:
236
- bot_instance.shutdown()
237
- bot_instance = None
238
- if session_id in session_data:
239
- del session_data[session_id]
240
 
241
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
242
- data = session_data.get(session_id, {})
243
- if not data:
244
- print("[Server Log] Session data not found for reporting.")
245
- return
246
-
247
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
248
-
249
  if results:
250
  result_df = pd.DataFrame(results).set_index('Name')
251
  full_df.set_index('Name', inplace=True)
252
  full_df['Status'] = full_df['Status'].astype('object')
253
- full_df.update(result_df)
254
- full_df.reset_index(inplace=True)
255
-
256
  full_df['Status'] = full_df['Status'].fillna('Not Processed')
 
257
  final_report_df = full_df[['Name', 'PRN', 'Status']]
258
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
259
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
260
 
261
- timestamp = datetime.now().strftime("%d_%b_%Y")
262
- custom_name = data.get('filename') or timestamp
263
-
264
- full_report_name = f"{custom_name}_Full.csv"
265
- bad_report_name = f"{custom_name}_Bad.csv"
266
- skipped_report_name = f"{custom_name}_Skipped.csv"
267
-
268
  full_report_content = final_report_df.to_csv(index=False)
269
  drive_service.upload_file(full_report_name, full_report_content)
270
-
271
  attachments = {
272
  full_report_name: full_report_content,
273
  bad_report_name: bad_df.to_csv(index=False),
274
  skipped_report_name: skipped_df.to_csv(index=False)
275
  }
276
-
277
  status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
278
-
279
  stats = {
280
- 'total': len(full_df),
281
- 'processed': len(results),
282
  'successful': len(full_df[full_df['Status'] == 'Done']),
283
- 'bad': len(bad_df),
284
- 'skipped': len(skipped_df)
285
  }
286
-
287
  process_type_str = data.get('mode', 'Unknown').title()
288
  subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
289
-
290
- professional_body = email_service.create_professional_email_template(
291
- subject, status_text, stats, custom_name, process_type_str
292
- )
293
-
294
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
295
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
296
 
297
- # =========================
298
- # HTTP Routes
299
- # =========================
300
-
301
  @app.route('/')
302
  def status_page():
303
  APP_STATUS_HTML = """<!DOCTYPE html><html lang="en"><head><title>API Status</title><style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f0f2f5;}.status-box{text-align:center;padding:40px 60px;background:white;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.1);}h1{font-size:24px;color:#333;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:8px 16px;border-radius:20px;}.active{color:#28a745;background-color:#e9f7ea;}</style></head><body><div class="status-box"><h1>Hillside Automation API</h1><div class="indicator active">● Active</div></div></body></html>"""
304
  return Response(APP_STATUS_HTML)
305
 
 
 
 
 
 
306
  @app.route('/process_and_initialize', methods=['POST'])
307
  def handle_file_processing_and_init():
308
- """
309
- HTTP alternative for uploading files (kept for compatibility).
310
- Emits both 'data_processed' and 'data_processed_and_ready' for client alignment.
311
- """
312
  session_id = 'user_session'
313
  try:
314
  data = session_data.get(session_id, {})
315
-
316
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
317
  return jsonify({"error": "Both files are required."}), 400
318
-
319
- # Read files
320
- df_app = (
321
- pd.read_excel(request.files['app_data'])
322
- if request.files['app_data'].filename.lower().endswith('.xlsx')
323
- else pd.read_csv(request.files['app_data'])
324
- )
325
- df_quantum = (
326
- pd.read_excel(request.files['quantum_data'])
327
- if request.files['quantum_data'].filename.lower().endswith('.xlsx')
328
- else pd.read_csv(request.files['quantum_data'])
329
- )
330
-
331
- # Validations
332
- if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
333
- raise ValueError("App Data file must contain 'Patient Name' and 'PRN' columns.")
334
- if 'Name' not in df_quantum.columns:
335
- raise ValueError("Quantum Data file must contain a 'Name' column.")
336
-
337
- # Filter out empty PRN rows using normalization (robust against lists/mixed types)
338
- df_app['__PN'] = df_app['Patient Name'].apply(normalize_str)
339
- df_app['__PRN'] = df_app['PRN'].apply(normalize_str)
340
- df_app_filtered = df_app[df_app['__PRN'] != ""].copy()
341
-
342
- # Create lookup: pure name (from Patient Name) -> PRN, using provided logic pattern
343
- prn_lookup_dict = create_prn_lookup(df_app_filtered.rename(columns={'__PN': 'Patient Name', '__PRN': 'PRN'}))
344
-
345
- # Lookup PRN for each Quantum name (match as-is but normalized to avoid list issues)
346
- df_quantum['__Name'] = df_quantum['Name'].apply(normalize_str)
347
- prn_list = [prn_lookup_dict.get(n, "") for n in df_quantum['__Name']]
348
-
349
- master_df = pd.DataFrame({
350
- 'Name': df_quantum['Name'],
351
- 'PRN': prn_list
352
- })
353
  master_df['Status'] = ''
354
-
355
  data['patient_data_for_report'] = master_df
356
  data['patient_data'] = master_df.to_dict('records')
357
- session_data[session_id] = data
358
-
359
- # Notify clients (both names for safety with current frontend)
360
  socketio.emit('data_processed')
361
- socketio.emit('data_processed_and_ready')
362
-
363
  return jsonify({"message": "Data processed successfully."})
364
  except Exception as e:
365
  print(f"[Server Log] ERROR during file processing: {e}")
366
  return jsonify({"error": str(e)}), 500
367
 
368
- # =========================
369
- # Socket.IO Events
370
- # =========================
371
-
372
  @socketio.on('connect')
373
  def handle_connect():
374
- print('Frontend connected.')
375
  emit('email_list', {'emails': get_email_list()})
376
 
377
  @socketio.on('initialize_session')
378
  def handle_init(data):
379
  session_id = 'user_session'
380
  session_data[session_id] = {
381
- 'emails': data.get('emails'),
382
- 'filename': data.get('filename'),
383
- 'mode': data.get('mode'),
384
- 'start_date': data.get('start_date'),
385
- 'end_date': data.get('end_date')
386
  }
387
-
388
- @socketio.on('process_and_initialize')
389
- def handle_process_and_initialize(payload):
390
- """
391
- NEW: Socket.IO path for the same workflow the UI currently uses.
392
- Decodes base64 files, builds master DF via provided logic, stores session, and emits 'data_processed_and_ready'.
393
- """
394
- try:
395
- session_id = 'user_session'
396
-
397
- def to_df(file_obj):
398
- name = file_obj.get('name', '')
399
- raw = base64.b64decode(file_obj['content'])
400
- if name.lower().endswith('.xlsx'):
401
- return pd.read_excel(io.BytesIO(raw))
402
- text = raw.decode('utf-8', errors='replace')
403
- return pd.read_csv(io.StringIO(text))
404
-
405
- df_app = to_df(payload['app_data_file'])
406
- df_quantum = to_df(payload['quantum_data_file'])
407
-
408
- # Validations
409
- if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
410
- emit('error', {'message': "App Data file must contain 'Patient Name' and 'PRN' columns."}, room=request.sid)
411
- return
412
- if 'Name' not in df_quantum.columns:
413
- emit('error', {'message': "Quantum Data file must contain a 'Name' column."}, room=request.sid)
414
- return
415
-
416
- # Filter out empty PRN rows using normalization (robust against lists/mixed types)
417
- df_app['__PN'] = df_app['Patient Name'].apply(normalize_str)
418
- df_app['__PRN'] = df_app['PRN'].apply(normalize_str)
419
- df_app_filtered = df_app[df_app['__PRN'] != ""].copy()
420
-
421
- # Create lookup: pure name (from Patient Name) -> PRN, using provided logic pattern
422
- prn_lookup_dict = create_prn_lookup(df_app_filtered.rename(columns={'__PN': 'Patient Name', '__PRN': 'PRN'}))
423
-
424
- # Lookup PRN for each Quantum name (match as-is but normalized to avoid list issues)
425
- df_quantum['__Name'] = df_quantum['Name'].apply(normalize_str)
426
- prn_list = [prn_lookup_dict.get(n, "") for n in df_quantum['__Name']]
427
-
428
- master_df = pd.DataFrame({
429
- 'Name': df_quantum['Name'],
430
- 'PRN': prn_list
431
- })
432
- master_df['Status'] = ''
433
-
434
- # Persist session data (includes meta from payload)
435
- session_data[session_id] = {
436
- 'emails': payload.get('emails', []),
437
- 'filename': payload.get('filename'),
438
- 'mode': payload.get('mode'),
439
- 'start_date': payload.get('start_date'),
440
- 'end_date': payload.get('end_date'),
441
- 'patient_data_for_report': master_df,
442
- 'patient_data': master_df.to_dict('records'),
443
- }
444
-
445
- # Tell this client to open the login modal
446
- emit('data_processed_and_ready', room=request.sid)
447
- except Exception as e:
448
- emit('error', {'message': f'Failed to process files: {e}'}, room=request.sid)
449
-
450
  @socketio.on('start_login')
451
  def handle_login(credentials):
452
  global bot_instance
453
- if bot_instance:
454
- bot_instance.shutdown()
455
  bot_instance = QuantumBot(socketio, app)
456
-
457
  is_success, error_message = bot_instance.initialize_driver()
458
  if is_success:
459
  is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
460
- if is_login_success:
461
- emit('otp_required')
462
- else:
463
- emit('error', {'message': f'Login failed: {login_error}'})
464
  else:
465
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
466
 
467
  @socketio.on('submit_otp')
468
  def handle_otp(data):
469
- if not bot_instance:
470
- return emit('error', {'message': 'Bot not initialized.'})
471
  is_success, error_message = bot_instance.submit_otp(data['otp'])
472
  if is_success:
473
  emit('login_successful')
474
  session_id = 'user_session'
475
  socketio.start_background_task(run_automation_process, session_id)
476
- else:
477
- emit('error', {'message': f'OTP failed: {error_message}'})
478
 
479
  @socketio.on('terminate_process')
480
  def handle_terminate():
481
- if bot_instance:
482
- print("Termination signal received.")
483
- bot_instance.stop()
484
-
485
- # =========================
486
- # Main
487
- # =========================
488
 
489
  if __name__ == '__main__':
490
  print("====================================================================")
@@ -492,4 +249,4 @@ if __name__ == '__main__':
492
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
493
  print(f" Port: {os.getenv('PORT', 7860)}")
494
  print("====================================================================")
495
- socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
 
 
1
  import eventlet
2
  eventlet.monkey_patch()
3
 
4
+ import pandas as pd
5
  import io
6
+ import threading
7
  import os
 
 
8
  import base64
9
+ import json
10
+ import re
11
  from datetime import datetime
 
 
12
  from flask import Flask, Response, request, jsonify
 
13
  from flask_socketio import SocketIO, emit
14
+ from flask_cors import CORS
 
15
  from worker import QuantumBot
16
+ from email.mime.multipart import MIMEMultipart
17
+ from email.mime.text import MIMEText
18
+ from email.mime.base import MIMEBase
19
+ from email import encoders
20
+ from dotenv import load_dotenv
21
 
22
+ load_dotenv()
 
 
23
 
24
  app = Flask(__name__)
25
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
 
26
  FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'https://quantbot.netlify.app')
 
27
  # --- Definitive CORS Fix ---
28
+ ALLOWED_ORIGINS = [os.getenv('FRONTEND_URL'), "http://localhost:3000", "http://127.0.0.1:5500", "null"]
29
+ ALLOWED_ORIGINS = [origin for origin in ALLOWED_ORIGINS if origin] # Filter out None values
 
 
 
 
 
30
 
31
  CORS(app, resources={r"/*": {"origins": ALLOWED_ORIGINS}})
32
  socketio = SocketIO(app, cors_allowed_origins=ALLOWED_ORIGINS, async_mode='eventlet')
33
 
 
34
  bot_instance = None
35
  session_data = {}
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  class GmailApiService:
38
  def __init__(self):
39
+ self.sender_email = os.getenv('EMAIL_SENDER'); self.service = None
 
40
  try:
41
+ from google.oauth2 import service_account; from googleapiclient.discovery import build
 
42
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
43
+ if not base64_creds: print("[Server Log] WARNING: GDRIVE_SA_KEY_BASE64 not found."); return
44
+ creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
45
+ credentials = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send'])
46
+ if self.sender_email: credentials = credentials.with_subject(self.sender_email)
 
 
 
 
 
 
47
  self.service = build('gmail', 'v1', credentials=credentials)
48
  print("[Server Log] Gmail API Service initialized successfully")
49
+ except Exception as e: print(f"[Server Log] Gmail API Error: {e}")
50
+
 
51
  def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
52
+ status_color = "#28a745" if "completed" in status_text else "#ffc107" if "terminated" in status_text else "#dc3545"
53
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
54
  html_template = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{subject}</title><style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2c2c2c; background-color: #f8f8f8; }} .email-container {{ max-width: 700px; margin: 20px auto; background-color: #ffffff; box-shadow: 0 8px 32px rgba(139, 0, 0, 0.15); border-radius: 12px; overflow: hidden; }} .header {{ background: linear-gradient(135deg, #8A0303 0%, #4c00ff 100%); color: white; padding: 40px 30px; text-align: center; }} .header h1 {{ font-size: 32px; font-weight: 700; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); }} .header p {{ font-size: 18px; opacity: 0.95; font-weight: 300; letter-spacing: 1px; }} .status-banner {{ background: {status_color}; color: white; text-align: center; padding: 18px; font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; }} .content {{ padding: 40px 30px; }} .report-info {{ background: #fdfdff; border-left: 6px solid #4c00ff; padding: 25px; margin-bottom: 35px; border-radius: 8px; box-shadow: 0 4px 12px rgba(76, 0, 255, 0.1); }} .info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px 25px; }} .info-item {{ display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }} .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px; margin: 35px 0; }} .stat-card {{ background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px 15px; text-align: center; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); }} .stat-number {{ font-size: 36px; font-weight: 700; color: #4c00ff; margin-bottom: 10px; }} .attachments-section {{ background: #f8f5ff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 25px; margin: 35px 0; }} .footer {{ background: #2c2c2c; color: white; padding: 35px 30px; text-align: center; }} h3 {{ color: #8A0303; margin-bottom: 20px; font-size: 20px; font-weight: 600; }} </style></head><body> <div class="email-container"> <div class="header"><h1>Hillside's Quantum Automation</h1><p>Patient Processing Report</p></div> <div class="status-banner">Process {status_text}</div> <div class="content"> <div class="report-info"> <h3>Report Summary</h3> <div class="info-grid"> <div class="info-item"><span><b>Report Name</b></span><span>{custom_name}</span></div> <div class="info-item"><span><b>Process Type</b></span><span>{process_type}</span></div> <div class="info-item"><span><b>Generated</b></span><span>{current_date}</span></div> <div class="info-item"><span><b>Status</b></span><span style="font-weight: bold; color: {status_color};">{status_text}</span></div> </div> </div> <h3>Processing Results</h3> <div class="stats-grid"> <div class="stat-card"><div class="stat-number">{stats['total']}</div><div>Total in File</div></div> <div class="stat-card"><div class="stat-number">{stats['processed']}</div><div>Attempted</div></div> <div class="stat-card"><div class="stat-number">{stats['successful']}</div><div>Done</div></div> <div class="stat-card"><div class="stat-number">{stats['bad']}</div><div>Bad State</div></div> <div class="stat-card"><div class="stat-number">{stats['skipped']}</div><div>Skipped</div></div> </div> <div class="attachments-section"> <h3>Attached Reports</h3> <p style="margin-bottom: 1rem;">The following files are attached and have been uploaded to Google Drive:</p> <ul style="list-style-position: inside; padding-left: 0;"> <li><b>{custom_name}_Full.csv:</b> The complete report with the final status for every patient.</li> <li><b>{custom_name}_Bad.csv:</b> A filtered list of patients that resulted in a "Bad" state.</li> <li><b>{custom_name}_Skipped.csv:</b> A filtered list of patients that were skipped.</li> </ul> </div> </div> <div class="footer"><h4>Hillside Automation</h4><p>This is an automated report. Please do not reply.</p></div> </div> </body></html>"""
55
  return html_template
56
 
57
  def send_report(self, recipients, subject, body, attachments=None):
58
+ if not self.service or not recipients: return False
 
59
  try:
60
+ from googleapiclient.errors import HttpError
61
+ message = MIMEMultipart(); message['From'] = self.sender_email; message['To'] = ', '.join(recipients); message['Subject'] = subject
 
 
 
 
 
 
 
62
  message.attach(MIMEText(body, 'html'))
 
63
  if attachments:
64
  for filename, content in attachments.items():
65
+ part = MIMEBase('application', 'octet-stream'); part.set_payload(content.encode('utf-8'))
66
+ encoders.encode_base64(part); part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
 
 
67
  message.attach(part)
 
68
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
69
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
70
+ print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}"); return True
71
+ except Exception as e: print(f"[Server Log] Failed to send email: {e}"); return False
 
 
 
72
 
73
  class GoogleDriveService:
74
  def __init__(self):
75
+ self.creds = None; self.service = None; self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
 
 
76
  try:
77
+ from google.oauth2 import service_account; from googleapiclient.discovery import build
 
 
78
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
79
+ if not base64_creds or not self.folder_id: raise ValueError("Google Drive secrets not found.")
80
+ creds_json = base64.b64decode(base64_creds).decode('utf-8'); creds_dict = json.loads(creds_json)
81
+ self.creds = service_account.Credentials.from_service_account_info(creds_dict, scopes=['https://www.googleapis.com/auth/drive'])
 
 
 
 
 
82
  self.service = build('drive', 'v3', credentials=self.creds)
83
  print("[Server Log] Google Drive Service initialized successfully.")
84
+ except Exception as e: print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
85
+
 
86
  def upload_file(self, filename, file_content):
87
+ if not self.service: return False
 
88
  try:
89
  from googleapiclient.http import MediaIoBaseUpload
90
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
91
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
92
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
93
+ print(f"[Server Log] File '{filename}' uploaded to Google Drive."); return True
94
+ except Exception as e: print(f"[Server Log] G-Drive ERROR: File upload failed: {e}"); return False
 
 
 
95
 
96
  email_service = GmailApiService()
97
  drive_service = GoogleDriveService()
98
 
99
+ def get_email_list():
100
+ try:
101
+ with open('config/emails.conf', 'r') as f: return [line.strip() for line in f if line.strip()]
102
+ except FileNotFoundError: return []
103
 
104
  def run_automation_process(session_id):
105
  global bot_instance
106
+ results = []; is_terminated = False; is_crash = False
 
 
107
  try:
108
+ data = session_data.get(session_id, {});
109
+ patient_data = data.get('patient_data'); mode = data.get('mode')
110
+ if not patient_data: raise ValueError("No patient data prepared for automation.")
 
 
 
 
111
  socketio.emit('initial_stats', {'total': len(patient_data)})
112
+ results = bot_instance.start_processing(mode, patient_data, start_date=data.get('start_date'), end_date=data.get('end_date'))
 
 
 
 
 
 
113
  is_terminated = bot_instance.termination_event.is_set()
114
  except Exception as e:
115
+ print(f"[Server Log] Fatal error in automation thread: {e}"); is_crash = True
 
116
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
117
  finally:
118
  socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
119
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
120
+ if bot_instance: bot_instance.shutdown(); bot_instance = None
121
+ if session_id in session_data: del session_data[session_id]
 
 
 
122
 
123
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
124
+ data = session_data.get(session_id, {});
125
+ if not data: print("[Server Log] Session data not found for reporting."); return
126
+
 
 
127
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
 
128
  if results:
129
  result_df = pd.DataFrame(results).set_index('Name')
130
  full_df.set_index('Name', inplace=True)
131
  full_df['Status'] = full_df['Status'].astype('object')
132
+ full_df.update(result_df); full_df.reset_index(inplace=True)
133
+
 
134
  full_df['Status'] = full_df['Status'].fillna('Not Processed')
135
+
136
  final_report_df = full_df[['Name', 'PRN', 'Status']]
137
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
138
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
139
 
140
+ timestamp = datetime.now().strftime("%d_%b_%Y"); custom_name = data.get('filename') or timestamp
141
+ full_report_name = f"{custom_name}_Full.csv"; bad_report_name = f"{custom_name}_Bad.csv"; skipped_report_name = f"{custom_name}_Skipped.csv"
 
 
 
 
 
142
  full_report_content = final_report_df.to_csv(index=False)
143
  drive_service.upload_file(full_report_name, full_report_content)
144
+
145
  attachments = {
146
  full_report_name: full_report_content,
147
  bad_report_name: bad_df.to_csv(index=False),
148
  skipped_report_name: skipped_df.to_csv(index=False)
149
  }
 
150
  status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
151
+
152
  stats = {
153
+ 'total': len(full_df), 'processed': len(results),
 
154
  'successful': len(full_df[full_df['Status'] == 'Done']),
155
+ 'bad': len(bad_df), 'skipped': len(skipped_df)
 
156
  }
 
157
  process_type_str = data.get('mode', 'Unknown').title()
158
  subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
159
+
160
+ professional_body = email_service.create_professional_email_template(subject, status_text, stats, custom_name, process_type_str)
161
+
 
 
162
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
163
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
164
 
 
 
 
 
165
  @app.route('/')
166
  def status_page():
167
  APP_STATUS_HTML = """<!DOCTYPE html><html lang="en"><head><title>API Status</title><style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f0f2f5;}.status-box{text-align:center;padding:40px 60px;background:white;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.1);}h1{font-size:24px;color:#333;margin-bottom:10px;} .indicator{font-size:18px;font-weight:600;padding:8px 16px;border-radius:20px;}.active{color:#28a745;background-color:#e9f7ea;}</style></head><body><div class="status-box"><h1>Hillside Automation API</h1><div class="indicator active">● Active</div></div></body></html>"""
168
  return Response(APP_STATUS_HTML)
169
 
170
+ def extract_patient_name(raw_name):
171
+ if not isinstance(raw_name, str): return ""
172
+ name_only = raw_name.split('DOB')[0].strip()
173
+ return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
174
+
175
  @app.route('/process_and_initialize', methods=['POST'])
176
  def handle_file_processing_and_init():
 
 
 
 
177
  session_id = 'user_session'
178
  try:
179
  data = session_data.get(session_id, {})
 
180
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
181
  return jsonify({"error": "Both files are required."}), 400
182
+
183
+ df_app = pd.read_excel(request.files['app_data']) if request.files['app_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['app_data'])
184
+ df_quantum = pd.read_excel(request.files['quantum_data']) if request.files['quantum_data'].filename.endswith('.xlsx') else pd.read_csv(request.files['quantum_data'])
185
+
186
+ if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns: raise ValueError("App Data must contain 'Patient Name' and 'PRN' columns.")
187
+ if 'Name' not in df_quantum.columns: raise ValueError("Quantum Data must contain a 'Name' column.")
188
+
189
+ df_app_filtered = df_app.dropna(subset=['PRN']); df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
190
+ prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
191
+
192
+ df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
193
+
194
+ master_df = df_quantum.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  master_df['Status'] = ''
196
+
197
  data['patient_data_for_report'] = master_df
198
  data['patient_data'] = master_df.to_dict('records')
199
+
 
 
200
  socketio.emit('data_processed')
 
 
201
  return jsonify({"message": "Data processed successfully."})
202
  except Exception as e:
203
  print(f"[Server Log] ERROR during file processing: {e}")
204
  return jsonify({"error": str(e)}), 500
205
 
 
 
 
 
206
  @socketio.on('connect')
207
  def handle_connect():
208
+ print(f'Frontend connected.')
209
  emit('email_list', {'emails': get_email_list()})
210
 
211
  @socketio.on('initialize_session')
212
  def handle_init(data):
213
  session_id = 'user_session'
214
  session_data[session_id] = {
215
+ 'emails': data['emails'], 'filename': data['filename'], 'mode': data.get('mode'),
216
+ 'start_date': data.get('start_date'), 'end_date': data.get('end_date')
 
 
 
217
  }
218
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  @socketio.on('start_login')
220
  def handle_login(credentials):
221
  global bot_instance
222
+ if bot_instance: bot_instance.shutdown()
 
223
  bot_instance = QuantumBot(socketio, app)
 
224
  is_success, error_message = bot_instance.initialize_driver()
225
  if is_success:
226
  is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
227
+ if is_login_success: emit('otp_required')
228
+ else: emit('error', {'message': f'Login failed: {login_error}'})
 
 
229
  else:
230
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
231
 
232
  @socketio.on('submit_otp')
233
  def handle_otp(data):
234
+ if not bot_instance: return emit('error', {'message': 'Bot not initialized.'})
 
235
  is_success, error_message = bot_instance.submit_otp(data['otp'])
236
  if is_success:
237
  emit('login_successful')
238
  session_id = 'user_session'
239
  socketio.start_background_task(run_automation_process, session_id)
240
+ else: emit('error', {'message': f'OTP failed: {error_message}'})
 
241
 
242
  @socketio.on('terminate_process')
243
  def handle_terminate():
244
+ if bot_instance: print("Termination signal received."); bot_instance.stop()
 
 
 
 
 
 
245
 
246
  if __name__ == '__main__':
247
  print("====================================================================")
 
249
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
250
  print(f" Port: {os.getenv('PORT', 7860)}")
251
  print("====================================================================")
252
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))