sonuprasad23 commited on
Commit
d972d64
·
1 Parent(s): c5136e8

Trying again

Browse files
Files changed (1) hide show
  1. server.py +281 -94
server.py CHANGED
@@ -1,247 +1,434 @@
 
 
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,4 +436,4 @@ if __name__ == '__main__':
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)))
 
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
+ if not isinstance(raw_name, str):
53
+ return ""
54
+ name_only = raw_name.split('DOB').strip()
55
+ return re.sub(r'[:\d\-\s]+$', '', name_only).strip()
56
+
57
+ def get_email_list():
58
+ try:
59
+ with open('config/emails.conf', 'r') as f:
60
+ return [line.strip() for line in f if line.strip()]
61
+ except FileNotFoundError:
62
+ return []
63
+
64
  class GmailApiService:
65
  def __init__(self):
66
+ self.sender_email = os.getenv('EMAIL_SENDER')
67
+ self.service = None
68
  try:
69
+ from google.oauth2 import service_account
70
+ from googleapiclient.discovery import build
71
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
72
+ if not base64_creds:
73
+ print("[Server Log] WARNING: GDRIVE_SA_KEY_BASE64 not found.")
74
+ return
75
+ creds_json = base64.b64decode(base64_creds).decode('utf-8')
76
+ creds_dict = json.loads(creds_json)
77
+ credentials = service_account.Credentials.from_service_account_info(
78
+ creds_dict, scopes=['https://www.googleapis.com/auth/gmail.send']
79
+ )
80
+ if self.sender_email:
81
+ credentials = credentials.with_subject(self.sender_email)
82
  self.service = build('gmail', 'v1', credentials=credentials)
83
  print("[Server Log] Gmail API Service initialized successfully")
84
+ except Exception as e:
85
+ print(f"[Server Log] Gmail API Error: {e}")
86
+
87
  def create_professional_email_template(self, subject, status_text, stats, custom_name, process_type):
88
+ status_color = "#28a745" if "completed" in status_text.lower() else "#ffc107" if "terminated" in status_text.lower() else "#dc3545"
89
  current_date = datetime.now().strftime("%B %d, %Y at %I:%M %p")
90
  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>"""
91
  return html_template
92
 
93
  def send_report(self, recipients, subject, body, attachments=None):
94
+ if not self.service or not recipients:
95
+ return False
96
  try:
97
+ from email.mime.multipart import MIMEMultipart
98
+ from email.mime.text import MIMEText
99
+ from email.mime.base import MIMEBase
100
+ from email import encoders
101
+
102
+ message = MIMEMultipart()
103
+ message['From'] = self.sender_email
104
+ message['To'] = ', '.join(recipients)
105
+ message['Subject'] = subject
106
  message.attach(MIMEText(body, 'html'))
107
+
108
  if attachments:
109
  for filename, content in attachments.items():
110
+ part = MIMEBase('application', 'octet-stream')
111
+ part.set_payload(content.encode('utf-8'))
112
+ encoders.encode_base64(part)
113
+ part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
114
  message.attach(part)
115
+
116
  raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
117
  sent_message = self.service.users().messages().send(userId='me', body={'raw': raw_message}).execute()
118
+ print(f"[Server Log] Email sent successfully! Message ID: {sent_message['id']}")
119
+ return True
120
+ except Exception as e:
121
+ print(f"[Server Log] Failed to send email: {e}")
122
+ return False
123
 
124
  class GoogleDriveService:
125
  def __init__(self):
126
+ self.creds = None
127
+ self.service = None
128
+ self.folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
129
  try:
130
+ from google.oauth2 import service_account
131
+ from googleapiclient.discovery import build
132
+
133
  base64_creds = os.getenv('GDRIVE_SA_KEY_BASE64')
134
+ if not base64_creds or not self.folder_id:
135
+ raise ValueError("Google Drive secrets not found.")
136
+
137
+ creds_json = base64.b64decode(base64_creds).decode('utf-8')
138
+ creds_dict = json.loads(creds_json)
139
+ self.creds = service_account.Credentials.from_service_account_info(
140
+ creds_dict, scopes=['https://www.googleapis.com/auth/drive']
141
+ )
142
  self.service = build('drive', 'v3', credentials=self.creds)
143
  print("[Server Log] Google Drive Service initialized successfully.")
144
+ except Exception as e:
145
+ print(f"[Server Log] G-Drive ERROR: Could not initialize service: {e}")
146
+
147
  def upload_file(self, filename, file_content):
148
+ if not self.service:
149
+ return False
150
  try:
151
  from googleapiclient.http import MediaIoBaseUpload
152
  file_metadata = {'name': filename, 'parents': [self.folder_id]}
153
  media = MediaIoBaseUpload(io.BytesIO(file_content.encode('utf-8')), mimetype='text/csv', resumable=True)
154
  self.service.files().create(body=file_metadata, media_body=media, fields='id').execute()
155
+ print(f"[Server Log] File '{filename}' uploaded to Google Drive.")
156
+ return True
157
+ except Exception as e:
158
+ print(f"[Server Log] G-Drive ERROR: File upload failed: {e}")
159
+ return False
160
 
161
  email_service = GmailApiService()
162
  drive_service = GoogleDriveService()
163
 
164
+ # =========================
165
+ # Core automation flow
166
+ # =========================
 
167
 
168
  def run_automation_process(session_id):
169
  global bot_instance
170
+ results = []
171
+ is_terminated = False
172
+ is_crash = False
173
  try:
174
+ data = session_data.get(session_id, {})
175
+ patient_data = data.get('patient_data')
176
+ mode = data.get('mode')
177
+
178
+ if not patient_data:
179
+ raise ValueError("No patient data prepared for automation.")
180
+
181
  socketio.emit('initial_stats', {'total': len(patient_data)})
182
+
183
+ results = bot_instance.start_processing(
184
+ mode,
185
+ patient_data,
186
+ start_date=data.get('start_date'),
187
+ end_date=data.get('end_date')
188
+ )
189
  is_terminated = bot_instance.termination_event.is_set()
190
  except Exception as e:
191
+ print(f"[Server Log] Fatal error in automation thread: {e}")
192
+ is_crash = True
193
  socketio.emit('error', {'message': f'A fatal error occurred: {e}'})
194
  finally:
195
  socketio.emit('micro_status_update', {'message': 'Generating final reports...'})
196
  generate_and_send_reports(session_id, results, is_crash_report=is_crash, is_terminated=is_terminated)
197
+ if bot_instance:
198
+ bot_instance.shutdown()
199
+ bot_instance = None
200
+ if session_id in session_data:
201
+ del session_data[session_id]
202
 
203
  def generate_and_send_reports(session_id, results, is_crash_report=False, is_terminated=False):
204
+ data = session_data.get(session_id, {})
205
+ if not data:
206
+ print("[Server Log] Session data not found for reporting.")
207
+ return
208
+
209
  full_df = pd.DataFrame(data.get('patient_data_for_report'))
210
+
211
  if results:
212
  result_df = pd.DataFrame(results).set_index('Name')
213
  full_df.set_index('Name', inplace=True)
214
  full_df['Status'] = full_df['Status'].astype('object')
215
+ full_df.update(result_df)
216
+ full_df.reset_index(inplace=True)
 
217
 
218
+ full_df['Status'] = full_df['Status'].fillna('Not Processed')
219
  final_report_df = full_df[['Name', 'PRN', 'Status']]
220
  bad_df = final_report_df[final_report_df['Status'] == 'Bad']
221
  skipped_df = final_report_df[final_report_df['Status'] == 'Skipped - No PRN']
222
 
223
+ timestamp = datetime.now().strftime("%d_%b_%Y")
224
+ custom_name = data.get('filename') or timestamp
225
+
226
+ full_report_name = f"{custom_name}_Full.csv"
227
+ bad_report_name = f"{custom_name}_Bad.csv"
228
+ skipped_report_name = f"{custom_name}_Skipped.csv"
229
+
230
  full_report_content = final_report_df.to_csv(index=False)
231
  drive_service.upload_file(full_report_name, full_report_content)
232
+
233
  attachments = {
234
  full_report_name: full_report_content,
235
  bad_report_name: bad_df.to_csv(index=False),
236
  skipped_report_name: skipped_df.to_csv(index=False)
237
  }
238
+
239
  status_text = "Terminated by User" if is_terminated else "Crashed" if is_crash_report else "Completed Successfully"
240
+
241
  stats = {
242
+ 'total': len(full_df),
243
+ 'processed': len(results),
244
  'successful': len(full_df[full_df['Status'] == 'Done']),
245
+ 'bad': len(bad_df),
246
+ 'skipped': len(skipped_df)
247
  }
248
+
249
  process_type_str = data.get('mode', 'Unknown').title()
250
  subject = f"{process_type_str} Automation Report [{status_text.upper()}]: {custom_name}"
251
+
252
+ professional_body = email_service.create_professional_email_template(
253
+ subject, status_text, stats, custom_name, process_type_str
254
+ )
255
+
256
  email_service.send_report(data.get('emails'), subject, professional_body, attachments)
257
  socketio.emit('process_complete', {'message': f'Process {status_text}. Report sent.'})
258
 
259
+ # =========================
260
+ # HTTP Routes
261
+ # =========================
262
+
263
  @app.route('/')
264
  def status_page():
265
  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>"""
266
  return Response(APP_STATUS_HTML)
267
 
 
 
 
 
 
268
  @app.route('/process_and_initialize', methods=['POST'])
269
  def handle_file_processing_and_init():
270
+ """
271
+ HTTP alternative for uploading files. Keeps backward compatibility.
272
+ It now emits both 'data_processed' and 'data_processed_and_ready' for client compatibility.
273
+ """
274
  session_id = 'user_session'
275
  try:
276
  data = session_data.get(session_id, {})
277
+
278
  if 'app_data' not in request.files or 'quantum_data' not in request.files:
279
  return jsonify({"error": "Both files are required."}), 400
280
+
281
+ # Read files
282
+ df_app = (
283
+ pd.read_excel(request.files['app_data'])
284
+ if request.files['app_data'].filename.lower().endswith('.xlsx')
285
+ else pd.read_csv(request.files['app_data'])
286
+ )
287
+ df_quantum = (
288
+ pd.read_excel(request.files['quantum_data'])
289
+ if request.files['quantum_data'].filename.lower().endswith('.xlsx')
290
+ else pd.read_csv(request.files['quantum_data'])
291
+ )
292
+
293
+ # Validations
294
+ if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
295
+ raise ValueError("App Data must contain 'Patient Name' and 'PRN' columns.")
296
+ if 'Name' not in df_quantum.columns:
297
+ raise ValueError("Quantum Data must contain a 'Name' column.")
298
+
299
+ # PRN mapping
300
+ df_app_filtered = df_app.dropna(subset=['PRN'])
301
+ df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
302
  prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
303
+
304
  df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
305
+
306
  master_df = df_quantum.copy()
307
  master_df['Status'] = ''
308
+
309
  data['patient_data_for_report'] = master_df
310
  data['patient_data'] = master_df.to_dict('records')
311
+ session_data[session_id] = data
312
+
313
+ # Notify clients
314
  socketio.emit('data_processed')
315
+ socketio.emit('data_processed_and_ready')
316
+
317
  return jsonify({"message": "Data processed successfully."})
318
  except Exception as e:
319
  print(f"[Server Log] ERROR during file processing: {e}")
320
  return jsonify({"error": str(e)}), 500
321
 
322
+ # =========================
323
+ # Socket.IO Events
324
+ # =========================
325
+
326
  @socketio.on('connect')
327
  def handle_connect():
328
+ print('Frontend connected.')
329
  emit('email_list', {'emails': get_email_list()})
330
 
331
  @socketio.on('initialize_session')
332
  def handle_init(data):
333
  session_id = 'user_session'
334
  session_data[session_id] = {
335
+ 'emails': data.get('emails'),
336
+ 'filename': data.get('filename'),
337
+ 'mode': data.get('mode'),
338
+ 'start_date': data.get('start_date'),
339
+ 'end_date': data.get('end_date')
340
  }
341
+
342
+ @socketio.on('process_and_initialize')
343
+ def handle_process_and_initialize(payload):
344
+ """
345
+ NEW: Socket.IO path for the same workflow the UI currently uses.
346
+ Decodes base64 files, builds master DF, stores session, and emits 'data_processed_and_ready'.
347
+ """
348
+ try:
349
+ session_id = 'user_session'
350
+
351
+ def to_df(file_obj):
352
+ name = file_obj.get('name', '')
353
+ raw = base64.b64decode(file_obj['content'])
354
+ if name.lower().endswith('.xlsx'):
355
+ return pd.read_excel(io.BytesIO(raw))
356
+ return pd.read_csv(io.StringIO(raw.decode('utf-8')))
357
+
358
+ df_app = to_df(payload['app_data_file'])
359
+ df_quantum = to_df(payload['quantum_data_file'])
360
+
361
+ # Validations
362
+ if 'Patient Name' not in df_app.columns or 'PRN' not in df_app.columns:
363
+ emit('error', {'message': "App Data must contain 'Patient Name' and 'PRN' columns."}, room=request.sid)
364
+ return
365
+ if 'Name' not in df_quantum.columns:
366
+ emit('error', {'message': "Quantum Data must contain a 'Name' column."}, room=request.sid)
367
+ return
368
+
369
+ # PRN mapping
370
+ df_app_filtered = df_app.dropna(subset=['PRN'])
371
+ df_app_filtered = df_app_filtered[df_app_filtered['PRN'].astype(str).str.strip() != '']
372
+ prn_lookup_dict = {extract_patient_name(row['Patient Name']): row['PRN'] for _, row in df_app_filtered.iterrows()}
373
+ df_quantum['PRN'] = df_quantum['Name'].apply(lambda name: prn_lookup_dict.get(name, ""))
374
+
375
+ master_df = df_quantum.copy()
376
+ master_df['Status'] = ''
377
+
378
+ # Persist session data (includes meta from payload)
379
+ session_data[session_id] = {
380
+ 'emails': payload.get('emails', []),
381
+ 'filename': payload.get('filename'),
382
+ 'mode': payload.get('mode'),
383
+ 'start_date': payload.get('start_date'),
384
+ 'end_date': payload.get('end_date'),
385
+ 'patient_data_for_report': master_df,
386
+ 'patient_data': master_df.to_dict('records'),
387
+ }
388
+
389
+ # Tell this client to open the login modal
390
+ emit('data_processed_and_ready', room=request.sid)
391
+ except Exception as e:
392
+ emit('error', {'message': f'Failed to process files: {e}'}, room=request.sid)
393
+
394
  @socketio.on('start_login')
395
  def handle_login(credentials):
396
  global bot_instance
397
+ if bot_instance:
398
+ bot_instance.shutdown()
399
  bot_instance = QuantumBot(socketio, app)
400
+
401
  is_success, error_message = bot_instance.initialize_driver()
402
  if is_success:
403
  is_login_success, login_error = bot_instance.login(credentials['username'], credentials['password'])
404
+ if is_login_success:
405
+ emit('otp_required')
406
+ else:
407
+ emit('error', {'message': f'Login failed: {login_error}'})
408
  else:
409
  emit('error', {'message': f'Failed to initialize bot: {error_message}'})
410
 
411
  @socketio.on('submit_otp')
412
  def handle_otp(data):
413
+ if not bot_instance:
414
+ return emit('error', {'message': 'Bot not initialized.'})
415
  is_success, error_message = bot_instance.submit_otp(data['otp'])
416
  if is_success:
417
  emit('login_successful')
418
  session_id = 'user_session'
419
  socketio.start_background_task(run_automation_process, session_id)
420
+ else:
421
+ emit('error', {'message': f'OTP failed: {error_message}'})
422
 
423
  @socketio.on('terminate_process')
424
  def handle_terminate():
425
+ if bot_instance:
426
+ print("Termination signal received.")
427
+ bot_instance.stop()
428
+
429
+ # =========================
430
+ # Main
431
+ # =========================
432
 
433
  if __name__ == '__main__':
434
  print("====================================================================")
 
436
  print(f" Frontend URL: {FRONTEND_ORIGIN}")
437
  print(f" Port: {os.getenv('PORT', 7860)}")
438
  print("====================================================================")
439
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))