lavanya121 commited on
Commit
ce7579b
·
verified ·
1 Parent(s): 92a0daa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -356
app.py CHANGED
@@ -3,31 +3,17 @@ import pandas as pd
3
  from datetime import datetime, timedelta
4
  import logging
5
  import plotly.express as px
 
6
  from sklearn.ensemble import IsolationForest
7
- from transformers import pipeline
8
- import torch
9
- from concurrent.futures import ThreadPoolExecutor
10
- from simple_salesforce import Salesforce
11
  import os
12
  import io
13
  import time
 
14
 
15
  # Configure logging
16
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
 
18
- # Salesforce configuration
19
- try:
20
- sf = Salesforce(
21
- username='multi-devicelabopsdashboard@sathkrutha.com',
22
- password='Team@1234',
23
- security_token=os.getenv('SF_SECURITY_TOKEN', ''),
24
- domain='login'
25
- )
26
- logging.info("Salesforce connection established")
27
- except Exception as e:
28
- logging.error(f"Failed to connect to Salesforce: {str(e)}")
29
- sf = None
30
-
31
  # Try to import reportlab
32
  try:
33
  from reportlab.lib.pagesizes import letter
@@ -40,220 +26,36 @@ except ImportError:
40
  logging.warning("reportlab module not found. PDF generation disabled.")
41
  reportlab_available = False
42
 
43
- # Preload Hugging Face model with optimization
44
- logging.info("Preloading Hugging Face model...")
45
- try:
46
- device = 0 if torch.cuda.is_available() else -1
47
- summarizer = pipeline(
48
- "summarization",
49
- model="t5-small",
50
- device=device,
51
- max_length=50,
52
- min_length=10,
53
- num_beams=2
54
- )
55
- logging.info(f"Hugging Face model preloaded on {'GPU' if device == 0 else 'CPU'}")
56
- except Exception as e:
57
- logging.error(f"Failed to preload model: {str(e)}")
58
- raise e
59
-
60
- # Cache picklist values at startup
61
- def get_picklist_values(field_name):
62
- if sf is None:
63
- return []
64
- try:
65
- obj_desc = sf.SmartLog__c.describe()
66
- for field in obj_desc['fields']:
67
- if field['name'] == field_name:
68
- return [value['value'] for value in field['picklistValues'] if value['active']]
69
- return []
70
- except Exception as e:
71
- logging.error(f"Failed to fetch picklist values for {field_name}: {str(e)}")
72
- return []
73
-
74
- status_values = get_picklist_values('Status__c') or ["Active", "Inactive", "Pending"]
75
- log_type_values = get_picklist_values('Log_Type__c') or ["Smart Log", "Cell Analysis", "UV Verification"]
76
- logging.info(f"Valid Status__c values: {status_values}")
77
- logging.info(f"Valid Log_Type__c values: {log_type_values}")
78
-
79
- # Map invalid picklist values
80
- picklist_mapping = {
81
- 'Status__c': {
82
- 'normal': 'Active',
83
- 'error': 'Inactive',
84
- 'warning': 'Pending',
85
- 'ok': 'Active',
86
- 'failed': 'Inactive'
87
- },
88
- 'Log_Type__c': {
89
- 'maint': 'Smart Log',
90
- 'error': 'Cell Analysis',
91
- 'ops': 'UV Verification',
92
- 'maintenance': 'Smart Log',
93
- 'cell': 'Cell Analysis',
94
- 'uv': 'UV Verification',
95
- 'weight log': 'Smart Log'
96
- }
97
- }
98
-
99
- # Cache folder ID
100
- def get_folder_id(folder_name):
101
- if sf is None:
102
- return None
103
- try:
104
- query = f"SELECT Id FROM Folder WHERE Name = '{folder_name}' AND Type = 'Report'"
105
- result = sf.query(query)
106
- if result['totalSize'] > 0:
107
- folder_id = result['records'][0]['Id']
108
- logging.info(f"Found folder ID for '{folder_name}': {folder_id}")
109
- return folder_id
110
- else:
111
- logging.error(f"Folder '{folder_name}' not found in Salesforce.")
112
- return None
113
- except Exception as e:
114
- logging.error(f"Failed to fetch folder ID for '{folder_name}': {str(e)}")
115
- return None
116
-
117
- LABOPS_REPORTS_FOLDER_ID = get_folder_id('LabOps Reports')
118
-
119
- # Salesforce report creation
120
- def create_salesforce_reports(df):
121
- if sf is None or not LABOPS_REPORTS_FOLDER_ID:
122
- return
123
- try:
124
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
125
- reports = [
126
- {
127
- "reportMetadata": {
128
- "name": f"SmartLog_Usage_Report_{timestamp}",
129
- "developerName": f"SmartLog_Usage_Report_{timestamp}",
130
- "reportType": {"type": "CustomEntity", "value": "SmartLog__c"},
131
- "reportFormat": "TABULAR",
132
- "reportBooleanFilter": None,
133
- "reportFilters": [],
134
- "detailColumns": ["SmartLog__c.Device_Id__c", "SmartLog__c.Usage_Hours__c"],
135
- "folderId": LABOPS_REPORTS_FOLDER_ID
136
- }
137
- },
138
- {
139
- "reportMetadata": {
140
- "name": f"SmartLog_AMC_Reminders_{timestamp}",
141
- "developerName": f"SmartLog_AMC_Reminders_{timestamp}",
142
- "reportType": {"type": "CustomEntity", "value": "SmartLog__c"},
143
- "reportFormat": "TABULAR",
144
- "reportBooleanFilter": None,
145
- "reportFilters": [],
146
- "detailColumns": ["SmartLog__c.Device_Id__c", "SmartLog__c.AMC_Date__c"],
147
- "folderId": LABOPS_REPORTS_FOLDER_ID
148
- }
149
- }
150
- ]
151
- for report in reports:
152
- sf.restful('analytics/reports', method='POST', json=report)
153
- logging.info("Salesforce reports created")
154
- except Exception as e:
155
- logging.error(f"Failed to create Salesforce reports: {str(e)}")
156
-
157
- # Save to Salesforce
158
- def save_to_salesforce(df, reminders_df):
159
- if sf is None:
160
- logging.error("No Salesforce connection available")
161
- return
162
- try:
163
- logging.info("Starting Salesforce save operation")
164
- current_date = datetime.now()
165
- next_30_days = current_date + timedelta(days=30)
166
- records = []
167
- reminder_device_ids = set(reminders_df['device_id']) if not reminders_df.empty else set()
168
- logging.info(f"Processing {len(df)} records for Salesforce")
169
-
170
- for idx, row in df.iterrows():
171
- status = str(row['status']).lower()
172
- log_type = str(row['log_type']).lower()
173
- status_mapped = picklist_mapping['Status__c'].get(status, status_values[0] if status_values else 'Active')
174
- log_type_mapped = picklist_mapping['Log_Type__c'].get(log_type, log_type_values[0] if log_type_values else 'Smart Log')
175
-
176
- if not status_mapped or not log_type_mapped:
177
- logging.warning(f"Skipping record {idx}: Invalid status ({status}) or log_type ({log_type})")
178
- continue
179
-
180
- amc_date_str = None
181
- if pd.notna(row['amc_date']):
182
- try:
183
- amc_date = pd.to_datetime(row['amc_date']).strftime('%Y-%m-%d')
184
- amc_date_str = amc_date
185
- amc_date_dt = datetime.strptime(amc_date, '%Y-%m-%d')
186
- if status_mapped == "Active" and current_date.date() <= amc_date_dt.date() <= next_30_days.date():
187
- logging.info(f"AMC Reminder for Device ID {row['device_id']}: {amc_date}")
188
- except Exception as e:
189
- logging.warning(f"Invalid AMC date for Device ID {row['device_id']}: {str(e)}")
190
-
191
- record = {
192
- 'Device_Id__c': str(row['device_id'])[:50],
193
- 'Log_Type__c': log_type_mapped,
194
- 'Status__c': status_mapped,
195
- 'Timestamp__c': row['timestamp'].isoformat() if pd.notna(row['timestamp']) else None,
196
- 'Usage_Hours__c': float(row['usage_hours']) if pd.notna(row['usage_hours']) else 0.0,
197
- 'Downtime__c': float(row['downtime']) if pd.notna(row['downtime']) else 0.0,
198
- 'AMC_Date__c': amc_date_str
199
- }
200
- records.append(record)
201
-
202
- if records:
203
- batch_size = 200 # Smaller batch size for faster processing
204
- for i in range(0, len(records), batch_size):
205
- batch = records[i:i + batch_size]
206
- try:
207
- result = sf.bulk.SmartLog__c.insert(batch)
208
- logging.info(f"Saved {len(batch)} records to Salesforce in batch {i//batch_size + 1}")
209
- for res in result:
210
- if not res['success']:
211
- logging.error(f"Failed to save record: {res['errors']}")
212
- except Exception as e:
213
- logging.error(f"Failed to save batch {i//batch_size + 1}: {str(e)}")
214
- else:
215
- logging.warning("No records to save to Salesforce")
216
- except Exception as e:
217
- logging.error(f"Failed to save to Salesforce: {str(e)}")
218
-
219
  # Summarize logs
220
  def summarize_logs(df):
221
- start_time = time.time()
222
  try:
223
  total_devices = df["device_id"].nunique()
224
- most_used = df.groupby("device_id")["usage_hours"].sum().idxmax() if not df.empty else "N/A"
225
- prompt = f"Maintenance logs: {total_devices} devices. Most used: {most_used}."
226
- summary = summarizer(prompt, max_length=50, min_length=10, do_sample=False)[0]["summary_text"]
227
- logging.info(f"Summary generation took {time.time() - start_time:.2f} seconds")
228
- return summary
229
  except Exception as e:
230
  logging.error(f"Summary generation failed: {str(e)}")
231
- return f"Failed to generate summary: {str(e)}"
232
 
233
  # Anomaly detection
234
  def detect_anomalies(df):
235
- start_time = time.time()
236
  try:
237
  if "usage_hours" not in df.columns or "downtime" not in df.columns:
238
  return "Anomaly detection requires 'usage_hours' and 'downtime' columns.", pd.DataFrame()
239
  features = df[["usage_hours", "downtime"]].fillna(0)
240
- if len(features) > 500:
241
- features = features.sample(n=500, random_state=42)
242
  iso_forest = IsolationForest(contamination=0.1, random_state=42)
243
  df["anomaly"] = iso_forest.fit_predict(features)
244
  anomalies = df[df["anomaly"] == -1][["device_id", "usage_hours", "downtime", "timestamp"]]
245
  if anomalies.empty:
246
  return "No anomalies detected.", anomalies
247
- result = "\n".join([f"- Device ID: {row['device_id']}, Usage: {row['usage_hours']}, Downtime: {row['downtime']}, Timestamp: {row['timestamp']}" for _, row in anomalies.head(5).iterrows()])
248
- logging.info(f"Anomaly detection took {time.time() - start_time:.2f} seconds")
249
- return result, anomalies
250
  except Exception as e:
251
  logging.error(f"Anomaly detection failed: {str(e)}")
252
  return f"Anomaly detection failed: {str(e)}", pd.DataFrame()
253
 
254
  # AMC reminders
255
  def check_amc_reminders(df, current_date):
256
- start_time = time.time()
257
  try:
258
  if "device_id" not in df.columns or "amc_date" not in df.columns:
259
  return "AMC reminders require 'device_id' and 'amc_date' columns.", pd.DataFrame()
@@ -263,32 +65,39 @@ def check_amc_reminders(df, current_date):
263
  reminders = df[(df["days_to_amc"] >= 0) & (df["days_to_amc"] <= 30)][["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]]
264
  if reminders.empty:
265
  return "No AMC reminders due within the next 30 days.", reminders
266
- result = "\n".join([f"- Device ID: {row['device_id']}, AMC Date: {row['amc_date']}" for _, row in reminders.head(5).iterrows()])
267
- logging.info(f"AMC reminders generation took {time.time() - start_time:.2f} seconds")
268
- return result, reminders
269
  except Exception as e:
270
  logging.error(f"AMC reminder generation failed: {str(e)}")
271
  return f"AMC reminder generation failed: {str(e)}", pd.DataFrame()
272
 
273
  # Dashboard insights
274
  def generate_dashboard_insights(df):
275
- start_time = time.time()
276
  try:
277
  total_devices = df["device_id"].nunique()
278
  avg_usage = df["usage_hours"].mean() if "usage_hours" in df.columns else 0
279
- prompt = f"Insights: {total_devices} devices, avg usage {avg_usage:.2f} hours."
280
- insights = summarizer(prompt, max_length=50, min_length=10, do_sample=False)[0]["summary_text"]
281
- logging.info(f"Insights generation took {time.time() - start_time:.2f} seconds")
282
- return insights
283
  except Exception as e:
284
  logging.error(f"Dashboard insights generation failed: {str(e)}")
285
- return f"Dashboard insights generation failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
  # Create usage chart
288
  def create_usage_chart(df):
289
  try:
290
- if df.empty:
291
- return None
 
292
  usage_data = df.groupby("device_id")["usage_hours"].sum().reset_index()
293
  if len(usage_data) > 5:
294
  usage_data = usage_data.nlargest(5, "usage_hours")
@@ -303,11 +112,14 @@ def create_usage_chart(df):
303
  return fig
304
  except Exception as e:
305
  logging.error(f"Failed to create usage chart: {str(e)}")
306
- return None
307
 
308
  # Create downtime chart
309
  def create_downtime_chart(df):
310
  try:
 
 
 
311
  downtime_data = df.groupby("device_id")["downtime"].sum().reset_index()
312
  if len(downtime_data) > 5:
313
  downtime_data = downtime_data.nlargest(5, "downtime")
@@ -322,13 +134,18 @@ def create_downtime_chart(df):
322
  return fig
323
  except Exception as e:
324
  logging.error(f"Failed to create downtime chart: {str(e)}")
325
- return None
326
 
327
  # Create daily log trends chart
328
  def create_daily_log_trends_chart(df):
329
  try:
330
- df['date'] = df['timestamp'].dt.date
 
 
 
331
  daily_logs = df.groupby('date').size().reset_index(name='log_count')
 
 
332
  fig = px.line(
333
  daily_logs,
334
  x='date',
@@ -340,19 +157,24 @@ def create_daily_log_trends_chart(df):
340
  return fig
341
  except Exception as e:
342
  logging.error(f"Failed to create daily log trends chart: {str(e)}")
343
- return None
344
 
345
  # Create weekly uptime chart
346
  def create_weekly_uptime_chart(df):
347
  try:
348
- df['week'] = df['timestamp'].dt.isocalendar().week
349
- df['year'] = df['timestamp'].dt.year
 
 
 
350
  weekly_data = df.groupby(['year', 'week']).agg({
351
  'usage_hours': 'sum',
352
  'downtime': 'sum'
353
  }).reset_index()
354
  weekly_data['uptime_percent'] = (weekly_data['usage_hours'] / (weekly_data['usage_hours'] + weekly_data['downtime'])) * 100
355
  weekly_data['year_week'] = weekly_data['year'].astype(str) + '-W' + weekly_data['week'].astype(str)
 
 
356
  fig = px.bar(
357
  weekly_data,
358
  x='year_week',
@@ -364,15 +186,18 @@ def create_weekly_uptime_chart(df):
364
  return fig
365
  except Exception as e:
366
  logging.error(f"Failed to create weekly uptime chart: {str(e)}")
367
- return None
368
 
369
  # Create anomaly alerts chart
370
  def create_anomaly_alerts_chart(anomalies_df):
371
  try:
372
- if anomalies_df.empty:
373
- return None
374
- anomalies_df['date'] = anomalies_df['timestamp'].dt.date
 
375
  anomaly_counts = anomalies_df.groupby('date').size().reset_index(name='anomaly_count')
 
 
376
  fig = px.scatter(
377
  anomaly_counts,
378
  x='date',
@@ -384,7 +209,7 @@ def create_anomaly_alerts_chart(anomalies_df):
384
  return fig
385
  except Exception as e:
386
  logging.error(f"Failed to create anomaly alerts chart: {str(e)}")
387
- return None
388
 
389
  # Generate device cards
390
  def generate_device_cards(df):
@@ -419,32 +244,12 @@ def generate_device_cards(df):
419
  logging.error(f"Failed to generate device cards: {str(e)}")
420
  return f'<p>Error generating device cards: {str(e)}</p>'
421
 
422
- # Generate monthly status
423
- def generate_monthly_status(df, selected_month):
424
- try:
425
- total_devices = df['device_id'].nunique()
426
- total_usage_hours = df['usage_hours'].sum()
427
- total_downtime = df['downtime'].sum()
428
- avg_usage = total_usage_hours / total_devices if total_devices > 0 else 0
429
- avg_downtime = total_downtime / total_devices if total_devices > 0 else 0
430
- return f"""
431
- Monthly Status for {selected_month}:
432
- - Total Devices: {total_devices}
433
- - Total Usage Hours: {total_usage_hours:.2f}
434
- - Total Downtime Hours: {total_downtime:.2f}
435
- - Average Usage per Device: {avg_usage:.2f} hours
436
- - Average Downtime per Device: {avg_downtime:.2f} hours
437
- """
438
- except Exception as e:
439
- logging.error(f"Failed to generate monthly status: {str(e)}")
440
- return f"Failed to generate monthly status: {str(e)}"
441
-
442
  # Generate PDF content
443
- def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards_html, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, df, selected_month):
444
  if not reportlab_available:
445
  return None
446
  try:
447
- pdf_path = f"monthly_status_report_{selected_month.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
448
  doc = SimpleDocTemplate(pdf_path, pagesize=letter)
449
  styles = getSampleStyleSheet()
450
  story = []
@@ -452,16 +257,10 @@ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights
452
  def safe_paragraph(text, style):
453
  return Paragraph(str(text).replace('\n', '<br/>'), style) if text else Paragraph("", style)
454
 
455
- story.append(Paragraph("LabOps Monthly Status Report", styles['Title']))
456
  story.append(Paragraph(f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal']))
457
  story.append(Spacer(1, 12))
458
 
459
- if selected_month != "All":
460
- monthly_status = generate_monthly_status(df, selected_month)
461
- story.append(Paragraph("Monthly Status Summary", styles['Heading2']))
462
- story.append(safe_paragraph(monthly_status, styles['Normal']))
463
- story.append(Spacer(1, 12))
464
-
465
  story.append(Paragraph("Summary Report", styles['Heading2']))
466
  story.append(safe_paragraph(summary, styles['Normal']))
467
  story.append(Spacer(1, 12))
@@ -516,111 +315,117 @@ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights
516
  return None
517
 
518
  # Main processing function
519
- async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_range, month_filter, last_modified_state):
520
  start_time = time.time()
521
  try:
522
  if not file_obj:
523
- return "No file uploaded.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, "No anomalies detected.", "No AMC reminders.", "No insights generated.", None, last_modified_state
524
 
525
  file_path = file_obj.name
526
  current_modified_time = os.path.getmtime(file_path)
527
- if last_modified_state and current_modified_time == last_modified_state:
528
- return None, None, None, None, None, None, None, None, None, None, None, None, last_modified_state
529
-
530
- logging.info(f"Processing file: {file_path}")
531
- if not file_path.endswith(".csv"):
532
- return "Please upload a CSV file.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, last_modified_state
533
-
534
- required_columns = ["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]
535
- dtypes = {
536
- "device_id": "string",
537
- "log_type": "string",
538
- "status": "string",
539
- "usage_hours": "float32",
540
- "downtime": "float32",
541
- "amc_date": "string"
542
- }
543
- df = pd.read_csv(file_path, dtype=dtypes)
544
- missing_columns = [col for col in required_columns if col not in df.columns]
545
- if missing_columns:
546
- return f"Missing columns: {missing_columns}", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state
547
-
548
- df["timestamp"] = pd.to_datetime(df["timestamp"], errors='coerce')
549
- df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
550
- if df["timestamp"].dt.tz is None:
551
- df["timestamp"] = df["timestamp"].dt.tz_localize('UTC').dt.tz_convert('Asia/Kolkata')
552
- if df.empty:
553
- return "No data available.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state
554
-
555
- # Apply filters
556
- filtered_df = df.copy()
557
- if lab_site_filter and lab_site_filter != 'All' and 'lab_site' in filtered_df.columns:
558
- filtered_df = filtered_df[filtered_df['lab_site'] == lab_site_filter]
559
- if equipment_type_filter and equipment_type_filter != 'All' and 'equipment_type' in filtered_df.columns:
560
- filtered_df = filtered_df[filtered_df['equipment_type'] == equipment_type_filter]
561
- if date_range and len(date_range) == 2:
562
- days_start, days_end = date_range
563
- today = pd.to_datetime(datetime.now().date()).tz_localize('Asia/Kolkata')
564
- start_date = today + pd.Timedelta(days=days_start)
565
- end_date = today + pd.Timedelta(days=days_end) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
566
- filtered_df = filtered_df[(filtered_df['timestamp'] >= start_date) & (filtered_df['timestamp'] <= end_date)]
567
- if month_filter and month_filter != "All":
568
- selected_date = pd.to_datetime(month_filter, format="%B %Y")
569
- filtered_df = filtered_df[
570
- (filtered_df['timestamp'].dt.year == selected_date.year) &
571
- (filtered_df['timestamp'].dt.month == selected_date.month)
572
- ]
573
-
574
- if filtered_df.empty:
575
- return "No data after applying filters.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state
576
 
577
  # Generate table for preview
578
  preview_df = filtered_df[['device_id', 'log_type', 'status', 'timestamp', 'usage_hours', 'downtime', 'amc_date']].head(5)
579
  preview_html = preview_df.to_html(index=False, classes='table table-striped', border=0)
580
 
581
- # Run tasks concurrently
582
- with ThreadPoolExecutor(max_workers=6) as executor:
583
- future_summary = executor.submit(summarize_logs, filtered_df)
584
- future_anomalies = executor.submit(detect_anomalies, filtered_df)
585
- future_amc = executor.submit(check_amc_reminders, filtered_df, datetime.now())
586
- future_insights = executor.submit(generate_dashboard_insights, filtered_df)
587
- future_usage_chart = executor.submit(create_usage_chart, filtered_df)
588
- future_downtime_chart = executor.submit(create_downtime_chart, filtered_df)
589
- future_daily_log_chart = executor.submit(create_daily_log_trends_chart, filtered_df)
590
- future_weekly_uptime_chart = executor.submit(create_weekly_uptime_chart, filtered_df)
591
- future_device_cards = executor.submit(generate_device_cards, filtered_df)
592
- future_reports = executor.submit(create_salesforce_reports, filtered_df)
593
-
594
- summary = f"Step 1: Summary Report\n{future_summary.result()}"
595
- anomalies, anomalies_df = future_anomalies.result()
596
- anomalies = f"Anomaly Detection\n{anomalies}"
597
- amc_reminders, reminders_df = future_amc.result()
598
- amc_reminders = f"AMC Reminders\n{amc_reminders}"
599
- insights = f"Dashboard Insights (AI)\n{future_insights.result()}"
600
- usage_chart = future_usage_chart.result()
601
- downtime_chart = future_downtime_chart.result()
602
- daily_log_chart = future_daily_log_chart.result()
603
- weekly_uptime_chart = future_weekly_uptime_chart.result()
604
- anomaly_alerts_chart = create_anomaly_alerts_chart(anomalies_df) # Use anomalies_df
605
- device_cards = future_device_cards.result()
606
-
607
- save_to_salesforce(filtered_df, reminders_df)
608
- pdf_file = generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, filtered_df, month_filter)
609
 
610
  elapsed_time = time.time() - start_time
611
  logging.info(f"Processing completed in {elapsed_time:.2f} seconds")
612
- if elapsed_time > 10:
613
- logging.warning(f"Processing time exceeded 10 seconds: {elapsed_time:.2f} seconds")
614
 
615
- return (summary, preview_html, usage_chart, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights, pdf_file, current_modified_time)
616
  except Exception as e:
617
  logging.error(f"Failed to process file: {str(e)}")
618
- return f"Error: {str(e)}", pd.DataFrame(), None, '<p>Error processing data.</p>', None, None, None, None, None, None, None, None, last_modified_state
 
 
 
 
 
 
 
 
 
 
619
 
620
  # Update filters
621
- def update_filters(file_obj):
622
- if not file_obj:
623
- return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All')
624
  try:
625
  with open(file_obj.name, 'rb') as f:
626
  csv_content = f.read().decode('utf-8')
@@ -629,12 +434,11 @@ def update_filters(file_obj):
629
 
630
  lab_site_options = ['All'] + [site for site in df['lab_site'].dropna().astype(str).unique().tolist() if site.strip()] if 'lab_site' in df.columns else ['All']
631
  equipment_type_options = ['All'] + [equip for equip in df['equipment_type'].dropna().astype(str).unique().tolist() if equip.strip()] if 'equipment_type' in df.columns else ['All']
632
- month_options = ['All'] + sorted(df['timestamp'].dt.strftime('%B %Y').dropna().unique().tolist()) if 'timestamp' in df.columns else ['All']
633
 
634
- return gr.update(choices=lab_site_options, value='All'), gr.update(choices=equipment_type_options, value='All'), gr.update(choices=month_options, value='All')
635
  except Exception as e:
636
  logging.error(f"Failed to update filters: {str(e)}")
637
- return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All')
638
 
639
  # Gradio Interface
640
  try:
@@ -651,10 +455,13 @@ try:
651
  .table th {background-color: #f2f2f2;}
652
  .table tr:nth-child(even) {background-color: #f9f9f9;}
653
  """) as iface:
654
- gr.Markdown("<h1>LabOps Log Analyzer Dashboard (Hugging Face AI)</h1>")
655
- gr.Markdown("Upload a CSV file to analyze. Click 'Analyze' to refresh the dashboard with the latest data.")
656
 
657
  last_modified_state = gr.State(value=None)
 
 
 
658
 
659
  with gr.Row():
660
  with gr.Column(scale=1):
@@ -664,7 +471,8 @@ try:
664
  lab_site_filter = gr.Dropdown(label="Lab Site", choices=['All'], value='All', interactive=True)
665
  equipment_type_filter = gr.Dropdown(label="Equipment Type", choices=['All'], value='All', interactive=True)
666
  date_range_filter = gr.Slider(label="Date Range (Days from Today)", minimum=-365, maximum=0, step=1, value=[-30, 0])
667
- submit_button = gr.Button("Analyze", variant="primary")
 
668
 
669
  with gr.Column(scale=2):
670
  with gr.Group(elem_classes="dashboard-container"):
@@ -697,23 +505,29 @@ try:
697
  gr.Markdown("### Step 5: AMC Reminders")
698
  amc_output = gr.Markdown()
699
  with gr.Group(elem_classes="dashboard-section"):
700
- gr.Markdown("### Step 6: Insights (AI)")
701
  insights_output = gr.Markdown()
702
  with gr.Group(elem_classes="dashboard-section"):
703
  gr.Markdown("### Export Report")
704
- pdf_output = gr.File(label="Download Monthly Status Report as PDF")
705
 
706
  file_input.change(
707
  fn=update_filters,
708
- inputs=[file_input],
709
- outputs=[lab_site_filter, equipment_type_filter],
710
  queue=False
711
  )
712
 
713
  submit_button.click(
714
  fn=process_logs,
715
- inputs=[file_input, lab_site_filter, equipment_type_filter, date_range_filter, last_modified_state],
716
- outputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, weekly_uptime_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output, pdf_output, last_modified_state]
 
 
 
 
 
 
717
  )
718
 
719
  logging.info("Gradio interface initialized successfully")
 
3
  from datetime import datetime, timedelta
4
  import logging
5
  import plotly.express as px
6
+ import plotly.graph_objects as go
7
  from sklearn.ensemble import IsolationForest
8
+ from concurrent.futures import ThreadPoolExecutor # Added missing import
 
 
 
9
  import os
10
  import io
11
  import time
12
+ import asyncio
13
 
14
  # Configure logging
15
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  # Try to import reportlab
18
  try:
19
  from reportlab.lib.pagesizes import letter
 
26
  logging.warning("reportlab module not found. PDF generation disabled.")
27
  reportlab_available = False
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  # Summarize logs
30
  def summarize_logs(df):
 
31
  try:
32
  total_devices = df["device_id"].nunique()
33
+ total_usage = df["usage_hours"].sum() if "usage_hours" in df.columns else 0
34
+ return f"{total_devices} devices processed with {total_usage:.2f} total usage hours."
 
 
 
35
  except Exception as e:
36
  logging.error(f"Summary generation failed: {str(e)}")
37
+ return "Failed to generate summary."
38
 
39
  # Anomaly detection
40
  def detect_anomalies(df):
 
41
  try:
42
  if "usage_hours" not in df.columns or "downtime" not in df.columns:
43
  return "Anomaly detection requires 'usage_hours' and 'downtime' columns.", pd.DataFrame()
44
  features = df[["usage_hours", "downtime"]].fillna(0)
45
+ if len(features) > 50:
46
+ features = features.sample(n=50, random_state=42)
47
  iso_forest = IsolationForest(contamination=0.1, random_state=42)
48
  df["anomaly"] = iso_forest.fit_predict(features)
49
  anomalies = df[df["anomaly"] == -1][["device_id", "usage_hours", "downtime", "timestamp"]]
50
  if anomalies.empty:
51
  return "No anomalies detected.", anomalies
52
+ return "\n".join([f"- Device ID: {row['device_id']}, Usage: {row['usage_hours']}, Downtime: {row['downtime']}, Timestamp: {row['timestamp']}" for _, row in anomalies.head(5).iterrows()]), anomalies
 
 
53
  except Exception as e:
54
  logging.error(f"Anomaly detection failed: {str(e)}")
55
  return f"Anomaly detection failed: {str(e)}", pd.DataFrame()
56
 
57
  # AMC reminders
58
  def check_amc_reminders(df, current_date):
 
59
  try:
60
  if "device_id" not in df.columns or "amc_date" not in df.columns:
61
  return "AMC reminders require 'device_id' and 'amc_date' columns.", pd.DataFrame()
 
65
  reminders = df[(df["days_to_amc"] >= 0) & (df["days_to_amc"] <= 30)][["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]]
66
  if reminders.empty:
67
  return "No AMC reminders due within the next 30 days.", reminders
68
+ return "\n".join([f"- Device ID: {row['device_id']}, AMC Date: {row['amc_date']}" for _, row in reminders.head(5).iterrows()]), reminders
 
 
69
  except Exception as e:
70
  logging.error(f"AMC reminder generation failed: {str(e)}")
71
  return f"AMC reminder generation failed: {str(e)}", pd.DataFrame()
72
 
73
  # Dashboard insights
74
  def generate_dashboard_insights(df):
 
75
  try:
76
  total_devices = df["device_id"].nunique()
77
  avg_usage = df["usage_hours"].mean() if "usage_hours" in df.columns else 0
78
+ return f"{total_devices} devices with average usage of {avg_usage:.2f} hours."
 
 
 
79
  except Exception as e:
80
  logging.error(f"Dashboard insights generation failed: {str(e)}")
81
+ return "Failed to generate insights."
82
+
83
+ # Placeholder chart for empty data
84
+ def create_placeholder_chart(title):
85
+ fig = go.Figure()
86
+ fig.add_annotation(
87
+ text="No data available for this chart",
88
+ xref="paper", yref="paper",
89
+ x=0.5, y=0.5, showarrow=False,
90
+ font=dict(size=16)
91
+ )
92
+ fig.update_layout(title=title, margin=dict(l=20, r=20, t=40, b=20))
93
+ return fig
94
 
95
  # Create usage chart
96
  def create_usage_chart(df):
97
  try:
98
+ if df.empty or "usage_hours" not in df.columns or "device_id" not in df.columns:
99
+ logging.warning("Insufficient data for usage chart")
100
+ return create_placeholder_chart("Usage Hours per Device")
101
  usage_data = df.groupby("device_id")["usage_hours"].sum().reset_index()
102
  if len(usage_data) > 5:
103
  usage_data = usage_data.nlargest(5, "usage_hours")
 
112
  return fig
113
  except Exception as e:
114
  logging.error(f"Failed to create usage chart: {str(e)}")
115
+ return create_placeholder_chart("Usage Hours per Device")
116
 
117
  # Create downtime chart
118
  def create_downtime_chart(df):
119
  try:
120
+ if df.empty or "downtime" not in df.columns or "device_id" not in df.columns:
121
+ logging.warning("Insufficient data for downtime chart")
122
+ return create_placeholder_chart("Downtime per Device")
123
  downtime_data = df.groupby("device_id")["downtime"].sum().reset_index()
124
  if len(downtime_data) > 5:
125
  downtime_data = downtime_data.nlargest(5, "downtime")
 
134
  return fig
135
  except Exception as e:
136
  logging.error(f"Failed to create downtime chart: {str(e)}")
137
+ return create_placeholder_chart("Downtime per Device")
138
 
139
  # Create daily log trends chart
140
  def create_daily_log_trends_chart(df):
141
  try:
142
+ if df.empty or "timestamp" not in df.columns:
143
+ logging.warning("Insufficient data for daily log trends chart")
144
+ return create_placeholder_chart("Daily Log Trends")
145
+ df['date'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.date
146
  daily_logs = df.groupby('date').size().reset_index(name='log_count')
147
+ if daily_logs.empty:
148
+ return create_placeholder_chart("Daily Log Trends")
149
  fig = px.line(
150
  daily_logs,
151
  x='date',
 
157
  return fig
158
  except Exception as e:
159
  logging.error(f"Failed to create daily log trends chart: {str(e)}")
160
+ return create_placeholder_chart("Daily Log Trends")
161
 
162
  # Create weekly uptime chart
163
  def create_weekly_uptime_chart(df):
164
  try:
165
+ if df.empty or "timestamp" not in df.columns or "usage_hours" not in df.columns or "downtime" not in df.columns:
166
+ logging.warning("Insufficient data for weekly uptime chart")
167
+ return create_placeholder_chart("Weekly Uptime Percentage")
168
+ df['week'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.isocalendar().week
169
+ df['year'] = pd.to_datetime(df['timestamp'], errors='coerce').dt.year
170
  weekly_data = df.groupby(['year', 'week']).agg({
171
  'usage_hours': 'sum',
172
  'downtime': 'sum'
173
  }).reset_index()
174
  weekly_data['uptime_percent'] = (weekly_data['usage_hours'] / (weekly_data['usage_hours'] + weekly_data['downtime'])) * 100
175
  weekly_data['year_week'] = weekly_data['year'].astype(str) + '-W' + weekly_data['week'].astype(str)
176
+ if weekly_data.empty:
177
+ return create_placeholder_chart("Weekly Uptime Percentage")
178
  fig = px.bar(
179
  weekly_data,
180
  x='year_week',
 
186
  return fig
187
  except Exception as e:
188
  logging.error(f"Failed to create weekly uptime chart: {str(e)}")
189
+ return create_placeholder_chart("Weekly Uptime Percentage")
190
 
191
  # Create anomaly alerts chart
192
  def create_anomaly_alerts_chart(anomalies_df):
193
  try:
194
+ if anomalies_df is None or anomalies_df.empty or "timestamp" not in anomalies_df.columns:
195
+ logging.warning("Insufficient data for anomaly alerts chart")
196
+ return create_placeholder_chart("Anomaly Alerts Over Time")
197
+ anomalies_df['date'] = pd.to_datetime(anomalies_df['timestamp'], errors='coerce').dt.date
198
  anomaly_counts = anomalies_df.groupby('date').size().reset_index(name='anomaly_count')
199
+ if anomaly_counts.empty:
200
+ return create_placeholder_chart("Anomaly Alerts Over Time")
201
  fig = px.scatter(
202
  anomaly_counts,
203
  x='date',
 
209
  return fig
210
  except Exception as e:
211
  logging.error(f"Failed to create anomaly alerts chart: {str(e)}")
212
+ return create_placeholder_chart("Anomaly Alerts Over Time")
213
 
214
  # Generate device cards
215
  def generate_device_cards(df):
 
244
  logging.error(f"Failed to generate device cards: {str(e)}")
245
  return f'<p>Error generating device cards: {str(e)}</p>'
246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  # Generate PDF content
248
+ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards_html, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart):
249
  if not reportlab_available:
250
  return None
251
  try:
252
+ pdf_path = f"status_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
253
  doc = SimpleDocTemplate(pdf_path, pagesize=letter)
254
  styles = getSampleStyleSheet()
255
  story = []
 
257
  def safe_paragraph(text, style):
258
  return Paragraph(str(text).replace('\n', '<br/>'), style) if text else Paragraph("", style)
259
 
260
+ story.append(Paragraph("LabOps Status Report", styles['Title']))
261
  story.append(Paragraph(f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal']))
262
  story.append(Spacer(1, 12))
263
 
 
 
 
 
 
 
264
  story.append(Paragraph("Summary Report", styles['Heading2']))
265
  story.append(safe_paragraph(summary, styles['Normal']))
266
  story.append(Spacer(1, 12))
 
315
  return None
316
 
317
  # Main processing function
318
+ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_range, last_modified_state, cached_df_state, cached_filtered_df_state):
319
  start_time = time.time()
320
  try:
321
  if not file_obj:
322
+ return "No file uploaded.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, "No anomalies detected.", "No AMC reminders.", "No insights generated.", None, last_modified_state, cached_df_state, cached_filtered_df_state
323
 
324
  file_path = file_obj.name
325
  current_modified_time = os.path.getmtime(file_path)
326
+ if last_modified_state and current_modified_time == last_modified_state and cached_filtered_df_state is not None:
327
+ filtered_df = cached_filtered_df_state
328
+ else:
329
+ if cached_df_state is None or current_modified_time != last_modified_state:
330
+ logging.info(f"Processing file: {file_path}")
331
+ if not file_path.endswith(".csv"):
332
+ return "Please upload a CSV file.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, "", "", "", None, last_modified_state, cached_df_state, cached_filtered_df_state
333
+
334
+ required_columns = ["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]
335
+ dtypes = {
336
+ "device_id": "string",
337
+ "log_type": "string",
338
+ "status": "string",
339
+ "usage_hours": "float32",
340
+ "downtime": "float32",
341
+ "amc_date": "string"
342
+ }
343
+ df = pd.read_csv(file_path, dtype=dtypes)
344
+ missing_columns = [col for col in required_columns if col not in df.columns]
345
+ if missing_columns:
346
+ return f"Missing columns: {missing_columns}", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state, cached_df_state, cached_filtered_df_state
347
+
348
+ df["timestamp"] = pd.to_datetime(df["timestamp"], errors='coerce')
349
+ df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
350
+ if df["timestamp"].dt.tz is None:
351
+ df["timestamp"] = df["timestamp"].dt.tz_localize('UTC').dt.tz_convert('Asia/Kolkata')
352
+ if df.empty:
353
+ return "No data available.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state, df, cached_filtered_df_state
354
+ else:
355
+ df = cached_df_state
356
+
357
+ # Apply filters
358
+ filtered_df = df.copy()
359
+ if lab_site_filter and lab_site_filter != 'All' and 'lab_site' in filtered_df.columns:
360
+ filtered_df = filtered_df[filtered_df['lab_site'] == lab_site_filter]
361
+ if equipment_type_filter and equipment_type_filter != 'All' and 'equipment_type' in filtered_df.columns:
362
+ filtered_df = filtered_df[filtered_df['equipment_type'] == equipment_type_filter]
363
+ if date_range and len(date_range) == 2:
364
+ days_start, days_end = date_range
365
+ today = pd.to_datetime(datetime.now().date()).tz_localize('Asia/Kolkata')
366
+ start_date = today + pd.Timedelta(days=days_start)
367
+ end_date = today + pd.Timedelta(days=days_end) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
368
+ filtered_df = filtered_df[(filtered_df['timestamp'] >= start_date) & (filtered_df['timestamp'] <= end_date)]
369
+
370
+ if filtered_df.empty:
371
+ return "No data after applying filters.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, None, last_modified_state, df, filtered_df
 
 
 
372
 
373
  # Generate table for preview
374
  preview_df = filtered_df[['device_id', 'log_type', 'status', 'timestamp', 'usage_hours', 'downtime', 'amc_date']].head(5)
375
  preview_html = preview_df.to_html(index=False, classes='table table-striped', border=0)
376
 
377
+ # Run critical tasks concurrently
378
+ try:
379
+ with ThreadPoolExecutor(max_workers=2) as executor:
380
+ future_anomalies = executor.submit(detect_anomalies, filtered_df)
381
+ future_amc = executor.submit(check_amc_reminders, filtered_df, datetime.now())
382
+
383
+ summary = f"Step 1: Summary Report\n{summarize_logs(filtered_df)}"
384
+ anomalies, anomalies_df = future_anomalies.result()
385
+ anomalies = f"Anomaly Detection\n{anomalies}"
386
+ amc_reminders, reminders_df = future_amc.result()
387
+ amc_reminders = f"AMC Reminders\n{amc_reminders}"
388
+ insights = f"Dashboard Insights\n{generate_dashboard_insights(filtered_df)}"
389
+ except Exception as e:
390
+ logging.error(f"Concurrent task execution failed: {str(e)}")
391
+ summary = "Failed to generate summary due to processing error."
392
+ anomalies = "Anomaly detection failed due to processing error."
393
+ amc_reminders = "AMC reminders failed due to processing error."
394
+ insights = "Insights generation failed due to processing error."
395
+ anomalies_df = pd.DataFrame()
396
+
397
+ # Generate charts sequentially
398
+ usage_chart = create_usage_chart(filtered_df)
399
+ downtime_chart = create_downtime_chart(filtered_df)
400
+ daily_log_chart = create_daily_log_trends_chart(filtered_df)
401
+ weekly_uptime_chart = create_weekly_uptime_chart(filtered_df)
402
+ anomaly_alerts_chart = create_anomaly_alerts_chart(anomalies_df)
403
+ device_cards = generate_device_cards(filtered_df)
 
404
 
405
  elapsed_time = time.time() - start_time
406
  logging.info(f"Processing completed in {elapsed_time:.2f} seconds")
407
+ if elapsed_time > 3:
408
+ logging.warning(f"Processing time exceeded 3 seconds: {elapsed_time:.2f} seconds")
409
 
410
+ return (summary, preview_html, usage_chart, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights, None, current_modified_time, df, filtered_df)
411
  except Exception as e:
412
  logging.error(f"Failed to process file: {str(e)}")
413
+ return f"Error: {str(e)}", pd.DataFrame(), None, '<p>Error processing data.</p>', None, None, None, None, None, None, None, None, last_modified_state, cached_df_state, cached_filtered_df_state
414
+
415
+ # Generate PDF separately
416
+ async def generate_pdf(summary, preview_html, usage_chart, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights):
417
+ try:
418
+ preview_df = pd.read_html(preview_html)[0]
419
+ pdf_file = generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards, daily_log_chart, weekly_uptime_chart, anomaly_alerts_chart, downtime_chart)
420
+ return pdf_file
421
+ except Exception as e:
422
+ logging.error(f"Failed to generate PDF: {str(e)}")
423
+ return None
424
 
425
  # Update filters
426
+ def update_filters(file_obj, current_file_state):
427
+ if not file_obj or file_obj.name == current_file_state:
428
+ return gr.update(), gr.update(), current_file_state
429
  try:
430
  with open(file_obj.name, 'rb') as f:
431
  csv_content = f.read().decode('utf-8')
 
434
 
435
  lab_site_options = ['All'] + [site for site in df['lab_site'].dropna().astype(str).unique().tolist() if site.strip()] if 'lab_site' in df.columns else ['All']
436
  equipment_type_options = ['All'] + [equip for equip in df['equipment_type'].dropna().astype(str).unique().tolist() if equip.strip()] if 'equipment_type' in df.columns else ['All']
 
437
 
438
+ return gr.update(choices=lab_site_options, value='All'), gr.update(choices=equipment_type_options, value='All'), file_obj.name
439
  except Exception as e:
440
  logging.error(f"Failed to update filters: {str(e)}")
441
+ return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All'), current_file_state
442
 
443
  # Gradio Interface
444
  try:
 
455
  .table th {background-color: #f2f2f2;}
456
  .table tr:nth-child(even) {background-color: #f9f9f9;}
457
  """) as iface:
458
+ gr.Markdown("<h1>LabOps Log Analyzer Dashboard</h1>")
459
+ gr.Markdown("Upload a CSV file to analyze. Click 'Analyze' to refresh the dashboard. Use 'Export PDF' for report download.")
460
 
461
  last_modified_state = gr.State(value=None)
462
+ current_file_state = gr.State(value=None)
463
+ cached_df_state = gr.State(value=None)
464
+ cached_filtered_df_state = gr.State(value=None)
465
 
466
  with gr.Row():
467
  with gr.Column(scale=1):
 
471
  lab_site_filter = gr.Dropdown(label="Lab Site", choices=['All'], value='All', interactive=True)
472
  equipment_type_filter = gr.Dropdown(label="Equipment Type", choices=['All'], value='All', interactive=True)
473
  date_range_filter = gr.Slider(label="Date Range (Days from Today)", minimum=-365, maximum=0, step=1, value=[-30, 0])
474
+ submit_button = gr.Button("Analyze", variant="primary")
475
+ pdf_button = gr.Button("Export PDF", variant="secondary")
476
 
477
  with gr.Column(scale=2):
478
  with gr.Group(elem_classes="dashboard-container"):
 
505
  gr.Markdown("### Step 5: AMC Reminders")
506
  amc_output = gr.Markdown()
507
  with gr.Group(elem_classes="dashboard-section"):
508
+ gr.Markdown("### Step 6: Insights")
509
  insights_output = gr.Markdown()
510
  with gr.Group(elem_classes="dashboard-section"):
511
  gr.Markdown("### Export Report")
512
+ pdf_output = gr.File(label="Download Status Report as PDF")
513
 
514
  file_input.change(
515
  fn=update_filters,
516
+ inputs=[file_input, current_file_state],
517
+ outputs=[lab_site_filter, equipment_type_filter, current_file_state],
518
  queue=False
519
  )
520
 
521
  submit_button.click(
522
  fn=process_logs,
523
+ inputs=[file_input, lab_site_filter, equipment_type_filter, date_range_filter, last_modified_state, cached_df_state, cached_filtered_df_state],
524
+ outputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, weekly_uptime_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output, pdf_output, last_modified_state, cached_df_state, cached_filtered_df_state]
525
+ )
526
+
527
+ pdf_button.click(
528
+ fn=generate_pdf,
529
+ inputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, weekly_uptime_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output],
530
+ outputs=[pdf_output]
531
  )
532
 
533
  logging.info("Gradio interface initialized successfully")