lavanya121 commited on
Commit
a578dc7
·
verified ·
1 Parent(s): fe9e1b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +567 -203
app.py CHANGED
@@ -1,293 +1,640 @@
1
- """
2
- LabOps Log Analyzer Dashboard with CSV file upload and optional PDF generation
3
- """
4
  import gradio as gr
5
  import pandas as pd
6
- from datetime import datetime
7
  import logging
8
  import plotly.express as px
9
  from sklearn.ensemble import IsolationForest
10
  from transformers import pipeline
11
  import torch
 
 
 
 
 
12
 
13
- # Try to import reportlab for PDF generation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
  from reportlab.lib.pagesizes import letter
16
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
17
  from reportlab.lib.styles import getSampleStyleSheet
 
18
  reportlab_available = True
19
  logging.info("reportlab module successfully imported")
20
  except ImportError:
21
- logging.warning("reportlab module not found. PDF generation will be disabled. Install with: pip install reportlab")
22
  reportlab_available = False
23
 
24
- # Configure logging for debugging
25
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
-
27
- # Preload Hugging Face summarization model at startup
28
  logging.info("Preloading Hugging Face model...")
29
  try:
30
  device = 0 if torch.cuda.is_available() else -1
31
- summarizer = pipeline("summarization", model="sshleifer/distilbart-cnn-12-6", device=device)
32
- logging.info(f"Hugging Face model preloaded successfully on device: {'GPU' if device == 0 else 'CPU'}")
 
 
 
 
 
 
 
33
  except Exception as e:
34
  logging.error(f"Failed to preload model: {str(e)}")
35
  raise e
36
 
37
- # Format summary prompt and generate report
38
- def summarize_logs(df, progress=gr.Progress()):
39
- progress(0.1, "Generating summary report...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  try:
41
  total_devices = df["device_id"].nunique()
42
  most_used = df.groupby("device_id")["usage_hours"].sum().idxmax() if not df.empty else "N/A"
43
  prompt = f"Maintenance logs: {total_devices} devices. Most used: {most_used}."
44
  summary = summarizer(prompt, max_length=50, min_length=10, do_sample=False)[0]["summary_text"]
45
- logging.info("Summary generated successfully")
46
  return summary
47
  except Exception as e:
48
  logging.error(f"Summary generation failed: {str(e)}")
49
  return f"Failed to generate summary: {str(e)}"
50
 
51
- # Anomaly Detection using Isolation Forest
52
- def detect_anomalies(df, progress=gr.Progress()):
53
- progress(0.4, "Detecting anomalies...")
54
  try:
55
  if "usage_hours" not in df.columns or "downtime" not in df.columns:
56
- logging.warning("Required columns for anomaly detection not found")
57
- return "Anomaly detection requires 'usage_hours' and 'downtime' columns."
58
- if len(df) > 5000:
59
- df = df.sample(n=5000, random_state=42)
60
- logging.info("Sampled data for anomaly detection to 5,000 rows")
61
  features = df[["usage_hours", "downtime"]].fillna(0)
62
- iso_forest = IsolationForest(contamination=0.1, random_state=42, n_jobs=-1)
 
 
63
  df["anomaly"] = iso_forest.fit_predict(features)
64
  anomalies = df[df["anomaly"] == -1][["device_id", "usage_hours", "downtime", "timestamp"]]
65
  if anomalies.empty:
66
- return "No anomalies detected."
67
- anomaly_lines = ["Detected Anomalies:"]
68
- for idx, row in anomalies.head(5).iterrows():
69
- anomaly_lines.append(f"- Device ID: {row['device_id']}, Usage Hours: {row['usage_hours']}, Downtime: {row['downtime']}, Timestamp: {row['timestamp']}")
70
- anomaly_list = "\n".join(anomaly_lines)
71
- logging.info("Anomalies detected successfully")
72
- return anomaly_list
73
  except Exception as e:
74
  logging.error(f"Anomaly detection failed: {str(e)}")
75
- return f"Anomaly detection failed: {str(e)}"
76
 
77
- # AMC Reminders based on device and AMC date
78
- def check_amc_reminders(df, current_date, progress=gr.Progress()):
79
- progress(0.6, "Checking AMC reminders...")
80
  try:
81
  if "device_id" not in df.columns or "amc_date" not in df.columns:
82
- logging.warning("Required columns for AMC reminders not found")
83
- return "AMC reminders require 'device_id' and 'amc_date' columns."
84
  df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
85
  current_date = pd.to_datetime(current_date)
86
  df["days_to_amc"] = (df["amc_date"] - current_date).dt.days
87
- reminders = df[(df["days_to_amc"] >= 0) & (df["days_to_amc"] <= 30)][["device_id", "amc_date"]]
88
  if reminders.empty:
89
- return "No AMC reminders due within the next 30 days."
90
- reminder_lines = ["Upcoming AMC Reminders:"]
91
- for idx, row in reminders.head(5).iterrows():
92
- reminder_lines.append(f"- Device ID: {row['device_id']}, AMC Date: {row['amc_date']}")
93
- reminder_list = "\n".join(reminder_lines)
94
- logging.info("AMC reminders generated successfully")
95
- return reminder_list
96
  except Exception as e:
97
  logging.error(f"AMC reminder generation failed: {str(e)}")
98
- return f"AMC reminder generation failed: {str(e)}"
99
 
100
- # Dashboard Insights (AI-generated)
101
- def generate_dashboard_insights(df, progress=gr.Progress()):
102
- progress(0.8, "Generating dashboard insights...")
103
  try:
104
  total_devices = df["device_id"].nunique()
105
  avg_usage = df["usage_hours"].mean() if "usage_hours" in df.columns else 0
106
  prompt = f"Insights: {total_devices} devices, avg usage {avg_usage:.2f} hours."
107
  insights = summarizer(prompt, max_length=50, min_length=10, do_sample=False)[0]["summary_text"]
108
- logging.info("Dashboard insights generated successfully")
109
  return insights
110
  except Exception as e:
111
  logging.error(f"Dashboard insights generation failed: {str(e)}")
112
  return f"Dashboard insights generation failed: {str(e)}"
113
 
114
- # Create a bar chart for usage hours per device
115
- def create_usage_chart(df, progress=gr.Progress()):
116
- progress(0.9, "Creating usage chart...")
117
  try:
 
 
118
  usage_data = df.groupby("device_id")["usage_hours"].sum().reset_index()
119
  if len(usage_data) > 5:
120
  usage_data = usage_data.nlargest(5, "usage_hours")
121
- logging.info("Limited chart data to top 5 devices")
122
- custom_colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']
123
  fig = px.bar(
124
  usage_data,
125
  x="device_id",
126
  y="usage_hours",
127
  title="Usage Hours per Device",
128
- labels={"device_id": "Device ID", "usage_hours": "Usage Hours"},
129
- color="device_id",
130
- color_discrete_sequence=custom_colors
131
- )
132
- fig.update_layout(
133
- title_font_size=16,
134
- margin=dict(l=20, r=20, t=40, b=20),
135
- plot_bgcolor="white",
136
- paper_bgcolor="white",
137
- font=dict(size=12)
138
  )
 
139
  return fig
140
  except Exception as e:
141
  logging.error(f"Failed to create usage chart: {str(e)}")
142
  return None
143
 
144
- # Generate PDF content using reportlab if available
145
- def generate_pdf_content(summary, preview, anomalies, amc_reminders, insights):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  if not reportlab_available:
147
- logging.warning("Skipping PDF generation: reportlab not installed")
148
  return None
149
  try:
150
- pdf_path = "analysis_report.pdf"
151
  doc = SimpleDocTemplate(pdf_path, pagesize=letter)
152
  styles = getSampleStyleSheet()
153
  story = []
154
 
155
- # Title
156
- story.append(Paragraph("LabOps Log Analysis Report", styles['Title']))
157
- story.append(Paragraph(f"Generated on {datetime.now().strftime('%Y-%m-%d')}", styles['Normal']))
 
 
158
  story.append(Spacer(1, 12))
159
 
160
- # Summary Report
 
 
 
 
 
161
  story.append(Paragraph("Summary Report", styles['Heading2']))
162
- for line in summary.split('\n'):
163
- story.append(Paragraph(line, styles['Normal']))
164
  story.append(Spacer(1, 12))
165
 
166
- # Log Preview
167
  story.append(Paragraph("Log Preview", styles['Heading2']))
168
- for line in preview.split('\n'):
169
- story.append(Paragraph(line, styles['Normal']))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  story.append(Spacer(1, 12))
171
 
172
- # Anomaly Detection
173
  story.append(Paragraph("Anomaly Detection", styles['Heading2']))
174
- for line in anomalies.split('\n'):
175
- story.append(Paragraph(line, styles['Normal']))
176
  story.append(Spacer(1, 12))
177
 
178
- # AMC Reminders
179
  story.append(Paragraph("AMC Reminders", styles['Heading2']))
180
- for line in amc_reminders.split('\n'):
181
- story.append(Paragraph(line, styles['Normal']))
182
  story.append(Spacer(1, 12))
183
 
184
- # Dashboard Insights
185
  story.append(Paragraph("Dashboard Insights", styles['Heading2']))
186
- for line in insights.split('\n'):
187
- story.append(Paragraph(line, styles['Normal']))
 
 
 
188
 
189
- # Build PDF
190
  doc.build(story)
191
- logging.info(f"PDF generated successfully at {pdf_path}")
192
  return pdf_path
193
  except Exception as e:
194
  logging.error(f"Failed to generate PDF: {str(e)}")
195
  return None
196
 
197
- # Main Gradio function
198
- async def process_logs(file_obj, progress=gr.Progress()):
 
199
  try:
200
- progress(0, "Starting file processing...")
201
  if not file_obj:
202
- logging.warning("No file uploaded, returning empty results")
203
- return "No file uploaded.", "No data to preview.", None, "No anomalies detected.", "No AMC reminders.", "No insights generated.", None
204
-
205
- file_name = file_obj.name
206
- logging.info(f"Processing file: {file_name}")
207
-
208
- if not file_name.endswith(".csv"):
209
- logging.error("Unsupported file format")
210
- return "Unsupported file format. Please upload a CSV file.", "", None, "", "", "", None
211
-
212
- try:
213
- required_columns = ["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]
214
- dtypes = {
215
- "device_id": "string",
216
- "log_type": "string",
217
- "status": "string",
218
- "usage_hours": "float32",
219
- "downtime": "float32",
220
- "amc_date": "string"
221
- }
222
- df = pd.read_csv(file_obj, dtype=dtypes)
223
- # Check for required columns
224
- missing_columns = [col for col in required_columns if col not in df.columns]
225
- if missing_columns:
226
- logging.error(f"Missing columns: {missing_columns}")
227
- return f"Missing required columns: {missing_columns}", None, None, None, None, None, None
228
- logging.info(f"File loaded successfully with {len(df)} rows")
229
- except Exception as e:
230
- logging.error(f"Failed to load CSV: {str(e)}")
231
- return f"Failed to load CSV: {str(e)}", None, None, None, None, None, None
232
-
233
- try:
234
- df["timestamp"] = pd.to_datetime(df["timestamp"], errors='coerce')
235
- except Exception as e:
236
- logging.error(f"Date conversion failed: {str(e)}")
237
- return f"Failed to convert timestamp to datetime: {str(e)}", None, None, None, None, None, None
238
-
239
  if df.empty:
240
- logging.warning("No data provided in the file")
241
- return "No data available in provided.", None, None, None, None, None, None
242
-
243
- # Step 1: Summary
244
- progress(0.2, "Summary...")
245
- summary = f"Step 1: Summary Report\n{summarize_logs(df)}"
246
-
247
- # Step 2: Log Preview
248
- progress(0.3, "Previewing...")
249
- if not df.empty:
250
- preview_lines = ["Step 2: Log Preview (First 5 Rows)"]
251
- for idx, row in df.head(5).iterrows():
252
- preview_lines.append(
253
- f"Row {idx + 1}: Device ID: {row['device_id']}, "
254
- f"Log Type: {row['log_type']}, Status: {row['status']}, "
255
- f"Timestamp: {row['timestamp']}, Usage Hours: {row['usage_hours']}, "
256
- f"Downtime: {row['downtime']}, AMC Date: {row['amc_date']}"
257
- )
258
- preview = "\n".join(preview_lines)
259
- else:
260
- preview = "Step 2: Log Preview\nNo data available."
261
-
262
- # Step 3: Usage Chart
263
- progress(0.5, "Chart...")
264
- chart = create_usage_chart(df)
265
-
266
- # Step 4: Anomaly Detection
267
- progress(0.7, "Anomalies...")
268
- anomalies = f"Anomaly Detection\n{detect_anomalies(df)}"
269
-
270
- # Step 5: AMC Reminders
271
- progress(0.8, "AMC...")
272
- amc_reminders = f"AMC Reminders\n{check_amc_reminders(df, datetime.now())}"
273
-
274
- # Step 6: Insights
275
- progress(0.9, "Insights...")
276
- insights = f"Dashboard Insights (AI)\n{generate_dashboard_insights(df)}"
277
-
278
- # Generate PDF if available
279
- progress(0.95, "PDF...")
280
- pdf_file = generate_pdf_content(summary, preview, anomalies, amc_reminders, insights) if reportlab_available else None
281
- if pdf_file is None and reportlab_available:
282
- logging.warning("PDF generation failed")
283
- elif pdf_file is None:
284
- logging.info("PDF skipped: no reportlab")
285
-
286
- progress(1.0, "Done!")
287
- return summary, preview, chart, anomalies, amc_reminders, insights, pdf_file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  except Exception as e:
289
  logging.error(f"Failed to process file: {str(e)}")
290
- return f"Error processing file: {str(e)}", None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  # Gradio Interface
293
  try:
@@ -299,58 +646,75 @@ try:
299
  .dashboard-section h3 {font-size: 18px; margin-bottom: 2px;}
300
  .dashboard-section p {margin: 1px 0; line-height: 1.2;}
301
  .dashboard-section ul {margin: 2px 0; padding-left: 20px;}
 
 
 
 
302
  """) as iface:
303
  gr.Markdown("<h1>LabOps Log Analyzer Dashboard (Hugging Face AI)</h1>")
304
- gr.Markdown("Upload a CSV file containing lab equipment logs to analyze.")
 
 
305
 
306
  with gr.Row():
307
  with gr.Column(scale=1):
308
  file_input = gr.File(label="Upload Logs (CSV)", file_types=[".csv"])
 
 
 
 
 
 
309
  submit_button = gr.Button("Analyze", variant="primary")
310
 
311
  with gr.Column(scale=2):
312
  with gr.Group(elem_classes="dashboard-container"):
313
- gr.Markdown("<div class='dashboard-title'>Analysis Results (Step-by-Step)</div>")
314
-
315
- # Step 1: Summary Report
316
  with gr.Group(elem_classes="dashboard-section"):
317
  gr.Markdown("### Step 1: Summary Report")
318
  summary_output = gr.Markdown()
319
-
320
- # Step 2: Log Preview
321
  with gr.Group(elem_classes="dashboard-section"):
322
  gr.Markdown("### Step 2: Log Preview")
323
- preview_output = gr.Markdown()
324
-
325
- # Step 3: Usage Chart
326
  with gr.Group(elem_classes="dashboard-section"):
327
- gr.Markdown("### Step 3: Usage Chart")
328
- chart_output = gr.Plot()
329
-
330
- # Step 4: Anomaly Detection
 
 
 
 
 
 
 
 
 
 
331
  with gr.Group(elem_classes="dashboard-section"):
332
  gr.Markdown("### Step 4: Anomaly Detection")
333
  anomaly_output = gr.Markdown()
334
-
335
- # Step 5: AMC Reminders
336
  with gr.Group(elem_classes="dashboard-section"):
337
  gr.Markdown("### Step 5: AMC Reminders")
338
  amc_output = gr.Markdown()
339
-
340
- # Step 6: Dashboard Insights
341
  with gr.Group(elem_classes="dashboard-section"):
342
  gr.Markdown("### Step 6: Insights (AI)")
343
  insights_output = gr.Markdown()
344
-
345
- # PDF Download
346
  with gr.Group(elem_classes="dashboard-section"):
347
- gr.Markdown("### Download Report")
348
- pdf_output = gr.File(label="Download Analysis Report as PDF")
 
 
 
 
 
 
 
349
 
350
  submit_button.click(
351
  fn=process_logs,
352
- inputs=[file_input],
353
- outputs=[summary_output, preview_output, chart_output, anomaly_output, amc_output, insights_output, pdf_output]
354
  )
355
 
356
  logging.info("Gradio interface initialized successfully")
 
 
 
 
1
  import gradio as gr
2
  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
34
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
35
  from reportlab.lib.styles import getSampleStyleSheet
36
+ from reportlab.lib import colors
37
  reportlab_available = True
38
  logging.info("reportlab module successfully imported")
39
  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()
 
260
  df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
261
  current_date = pd.to_datetime(current_date)
262
  df["days_to_amc"] = (df["amc_date"] - current_date).dt.days
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")
 
 
295
  fig = px.bar(
296
  usage_data,
297
  x="device_id",
298
  y="usage_hours",
299
  title="Usage Hours per Device",
300
+ labels={"device_id": "Device ID", "usage_hours": "Usage Hours"}
 
 
 
 
 
 
 
 
 
301
  )
302
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
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")
314
+ fig = px.bar(
315
+ downtime_data,
316
+ x="device_id",
317
+ y="downtime",
318
+ title="Downtime per Device",
319
+ labels={"device_id": "Device ID", "downtime": "Downtime (Hours)"}
320
+ )
321
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
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',
335
+ y='log_count',
336
+ title="Daily Log Trends",
337
+ labels={"date": "Date", "log_count": "Number of Logs"}
338
+ )
339
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
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',
359
+ y='uptime_percent',
360
+ title="Weekly Uptime Percentage",
361
+ labels={"year_week": "Year-Week", "uptime_percent": "Uptime %"}
362
+ )
363
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
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',
379
+ y='anomaly_count',
380
+ title="Anomaly Alerts Over Time",
381
+ labels={"date": "Date", "anomaly_count": "Number of Anomalies"}
382
+ )
383
+ fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
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):
391
+ try:
392
+ if df.empty:
393
+ return '<p>No devices available to display.</p>'
394
+ device_stats = df.groupby('device_id').agg({
395
+ 'status': 'last',
396
+ 'timestamp': 'max',
397
+ }).reset_index()
398
+ device_stats['count'] = df.groupby('device_id').size().reindex(device_stats['device_id']).values
399
+ device_stats['health'] = device_stats['status'].map({
400
+ 'Active': 'Healthy',
401
+ 'Inactive': 'Unhealthy',
402
+ 'Pending': 'Warning'
403
+ }).fillna('Unknown')
404
+ cards_html = '<div style="display: flex; flex-wrap: wrap; gap: 20px;">'
405
+ for _, row in device_stats.iterrows():
406
+ health_color = {'Healthy': 'green', 'Unhealthy': 'red', 'Warning': 'orange', 'Unknown': 'gray'}.get(row['health'], 'gray')
407
+ timestamp_str = str(row['timestamp']) if pd.notna(row['timestamp']) else 'Unknown'
408
+ cards_html += f"""
409
+ <div style="border: 1px solid #e0e0e0; padding: 10px; border-radius: 5px; width: 200px;">
410
+ <h4>Device: {row['device_id']}</h4>
411
+ <p><b>Health:</b> <span style="color: {health_color}">{row['health']}</span></p>
412
+ <p><b>Usage Count:</b> {row['count']}</p>
413
+ <p><b>Last Log:</b> {timestamp_str}</p>
414
+ </div>
415
+ """
416
+ cards_html += '</div>'
417
+ return cards_html
418
+ except Exception as e:
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 = []
451
 
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))
468
 
 
469
  story.append(Paragraph("Log Preview", styles['Heading2']))
470
+ if not preview_df.empty:
471
+ data = [preview_df.columns.tolist()] + preview_df.head(5).values.tolist()
472
+ table = Table(data)
473
+ table.setStyle(TableStyle([
474
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
475
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
476
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
477
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
478
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
479
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
480
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
481
+ ('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
482
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
483
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
484
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
485
+ ]))
486
+ story.append(table)
487
+ else:
488
+ story.append(safe_paragraph("No preview available.", styles['Normal']))
489
+ story.append(Spacer(1, 12))
490
+
491
+ story.append(Paragraph("Device Cards", styles['Heading2']))
492
+ device_cards_text = device_cards_html.replace('<div>', '').replace('</div>', '\n').replace('<h4>', '').replace('</h4>', '\n').replace('<p>', '').replace('</p>', '\n').replace('<b>', '').replace('</b>', '').replace('<span style="color: green">', '').replace('<span style="color: red">', '').replace('<span style="color: orange">', '').replace('<span style="color: gray">', '').replace('</span>', '')
493
+ story.append(safe_paragraph(device_cards_text, styles['Normal']))
494
  story.append(Spacer(1, 12))
495
 
 
496
  story.append(Paragraph("Anomaly Detection", styles['Heading2']))
497
+ story.append(safe_paragraph(anomalies, styles['Normal']))
 
498
  story.append(Spacer(1, 12))
499
 
 
500
  story.append(Paragraph("AMC Reminders", styles['Heading2']))
501
+ story.append(safe_paragraph(amc_reminders, styles['Normal']))
 
502
  story.append(Spacer(1, 12))
503
 
 
504
  story.append(Paragraph("Dashboard Insights", styles['Heading2']))
505
+ story.append(safe_paragraph(insights, styles['Normal']))
506
+ story.append(Spacer(1, 12))
507
+
508
+ story.append(Paragraph("Charts", styles['Heading2']))
509
+ story.append(Paragraph("[Chart placeholders - see dashboard for visuals]", styles['Normal']))
510
 
 
511
  doc.build(story)
512
+ logging.info(f"PDF generated at {pdf_path}")
513
  return pdf_path
514
  except Exception as e:
515
  logging.error(f"Failed to generate PDF: {str(e)}")
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')
627
+ df = pd.read_csv(io.StringIO(csv_content))
628
+ df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
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:
 
646
  .dashboard-section h3 {font-size: 18px; margin-bottom: 2px;}
647
  .dashboard-section p {margin: 1px 0; line-height: 1.2;}
648
  .dashboard-section ul {margin: 2px 0; padding-left: 20px;}
649
+ .table {width: 100%; border-collapse: collapse;}
650
+ .table th, .table td {border: 1px solid #ddd; padding: 8px; text-align: left;}
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):
661
  file_input = gr.File(label="Upload Logs (CSV)", file_types=[".csv"])
662
+ with gr.Group():
663
+ gr.Markdown("### Filters")
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
+ month_filter = gr.Dropdown(label="Select Month for Report", choices=['All'], value='All', interactive=True)
668
  submit_button = gr.Button("Analyze", variant="primary")
669
 
670
  with gr.Column(scale=2):
671
  with gr.Group(elem_classes="dashboard-container"):
672
+ gr.Markdown("<div class='dashboard-title'>Analysis Results</div>")
 
 
673
  with gr.Group(elem_classes="dashboard-section"):
674
  gr.Markdown("### Step 1: Summary Report")
675
  summary_output = gr.Markdown()
 
 
676
  with gr.Group(elem_classes="dashboard-section"):
677
  gr.Markdown("### Step 2: Log Preview")
678
+ preview_output = gr.HTML()
 
 
679
  with gr.Group(elem_classes="dashboard-section"):
680
+ gr.Markdown("### Device Cards")
681
+ device_cards_output = gr.HTML()
682
+ with gr.Group(elem_classes="dashboard-section"):
683
+ gr.Markdown("### Charts")
684
+ with gr.Tab("Usage Hours per Device"):
685
+ usage_chart_output = gr.Plot()
686
+ with gr.Tab("Downtime per Device"):
687
+ downtime_chart_output = gr.Plot()
688
+ with gr.Tab("Daily Log Trends"):
689
+ daily_log_trends_output = gr.Plot()
690
+ with gr.Tab("Weekly Uptime Percentage"):
691
+ weekly_uptime_output = gr.Plot()
692
+ with gr.Tab("Anomaly Alerts"):
693
+ anomaly_alerts_output = gr.Plot()
694
  with gr.Group(elem_classes="dashboard-section"):
695
  gr.Markdown("### Step 4: Anomaly Detection")
696
  anomaly_output = gr.Markdown()
 
 
697
  with gr.Group(elem_classes="dashboard-section"):
698
  gr.Markdown("### Step 5: AMC Reminders")
699
  amc_output = gr.Markdown()
 
 
700
  with gr.Group(elem_classes="dashboard-section"):
701
  gr.Markdown("### Step 6: Insights (AI)")
702
  insights_output = gr.Markdown()
 
 
703
  with gr.Group(elem_classes="dashboard-section"):
704
+ gr.Markdown("### Export Report")
705
+ pdf_output = gr.File(label="Download Monthly Status Report as PDF")
706
+
707
+ file_input.change(
708
+ fn=update_filters,
709
+ inputs=[file_input],
710
+ outputs=[lab_site_filter, equipment_type_filter, month_filter],
711
+ queue=False
712
+ )
713
 
714
  submit_button.click(
715
  fn=process_logs,
716
+ inputs=[file_input, lab_site_filter, equipment_type_filter, date_range_filter, month_filter, last_modified_state],
717
+ 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]
718
  )
719
 
720
  logging.info("Gradio interface initialized successfully")