MaheshP98 commited on
Commit
eadd170
·
verified ·
1 Parent(s): 630e8aa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -86
app.py CHANGED
@@ -15,77 +15,101 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s,%(msecs)03d - %(leve
15
 
16
  # CSS styling for the Gradio interface
17
  css = """
 
 
 
18
  body {
19
- font-family: Arial, sans-serif;
20
- background-color: #F3F4F6;
21
- color: #1E3A8A;
 
 
22
  }
23
 
24
  h1 {
25
- color: #1E3A8A;
26
  text-align: center;
27
- margin-bottom: 20px;
 
28
  }
29
 
30
  .gr-button {
31
- background-color: #1E3A8A;
32
  color: white;
33
  border: none;
34
- border-radius: 5px;
35
- padding: 10px 20px;
 
 
36
  }
37
 
38
  .gr-button:hover {
39
- background-color: #2B4C9B;
40
  }
41
 
42
- .summary-card {
 
 
 
 
 
 
 
 
43
  background-color: white;
44
  border-radius: 10px;
45
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
46
  padding: 20px;
47
- margin: 20px 0;
 
 
 
 
48
  }
49
 
50
- .summary-card h2 {
51
- color: #1E3A8A;
 
52
  margin-top: 0;
 
 
 
 
53
  }
54
 
55
- .maintenance-alert {
56
- background-color: white;
57
- border-radius: 10px;
58
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
59
- padding: 15px;
60
- margin: 10px 0;
61
  }
62
 
63
- .alert-urgent {
64
- color: #DC2626;
65
- font-weight: bold;
66
  }
67
 
68
- .alert-upcoming {
69
- color: #F59E0B;
70
- font-weight: bold;
71
  }
72
 
73
- .recommendation {
74
- font-style: italic;
75
- color: #4B5563;
 
 
 
 
 
76
  }
77
 
78
  .flowchart {
79
  display: flex;
80
  flex-direction: column;
81
  gap: 10px;
82
- margin: 20px 0;
83
  }
84
 
85
  .flowchart-step {
86
- background-color: #E5E7EB;
87
- border-left: 5px solid #1E3A8A;
88
- padding: 10px;
89
  border-radius: 5px;
90
  position: relative;
91
  }
@@ -97,15 +121,83 @@ h1 {
97
  left: 50%;
98
  transform: translateX(-50%);
99
  font-size: 20px;
100
- color: #1E3A8A;
101
  }
102
 
103
- .report-preview {
104
- background-color: white;
105
- border-radius: 10px;
106
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
107
- padding: 15px;
108
- margin: 10px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
  """
111
 
@@ -126,6 +218,31 @@ def validate_csv(df):
126
  return False, f"Invalid data types: {str(e)}"
127
  return True, ""
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
130
  """
131
  Generate a detailed and easy-to-understand summary of the processing results.
@@ -138,21 +255,21 @@ def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
138
  total_records = len(combined_df)
139
  unique_devices = combined_df['equipment'].unique()
140
  summary.append(f"We processed **{total_records} log entries** for **{len(unique_devices)} devices** ({', '.join(unique_devices)}).")
141
- summary.append("This report helps you understand device usage, identify unusual activity, and plan maintenance.\n")
142
 
143
- # Unusual Activity (Anomalies)
144
- summary.append("## Unusual Activity")
145
  if anomaly_df is not None:
146
  num_anomalies = sum(anomaly_df['anomaly'] == -1)
147
  if num_anomalies > 0:
148
- summary.append(f"We found **{num_anomalies} unusual activities** that might need your attention:")
149
  anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
150
  for _, row in anomaly_records.iterrows():
151
- summary.append(f"- **{row['equipment']}** (Usage: {row['usage_count']}, Status: {row['status']}) - High or low usage compared to others might indicate overuse or underuse.")
152
  else:
153
- summary.append("No unusual activity detected. All devices are operating within expected usage patterns.")
154
  else:
155
- summary.append("We couldn’t check for unusual activity due to an error.")
156
  summary.append("\n")
157
 
158
  # Maintenance Alerts (AMC Expiries)
@@ -173,7 +290,7 @@ def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
173
  # Generated Reports
174
  summary.append("## Generated Reports")
175
  summary.append("- **Usage Chart**: Visualizes usage patterns across devices, helping identify overworked or underused equipment. See below for the chart.")
176
- summary.append("- **PDF Report**: A comprehensive report including a full data table, unusual activity details, maintenance alerts, and a detailed flowchart of our process. Download it below.")
177
 
178
  return "\n".join(summary)
179
 
@@ -186,7 +303,7 @@ def generate_flowchart_html():
186
  ("Upload CSV File(s)", "User uploads log files in CSV format."),
187
  ("Validate Data", "Checks for required columns (equipment, usage_count, status, amc_expiry) and correct data types."),
188
  ("Generate Usage Chart", "Creates a bar chart showing usage counts by device and status (e.g., Active, Inactive)."),
189
- ("Detect Unusual Activity", "Uses Local Outlier Factor to identify devices with unusual usage patterns (e.g., too high or too low)."),
190
  ("Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05."),
191
  ("Create PDF Report", "Generates a detailed PDF with data tables, insights, and this flowchart.")
192
  ]
@@ -199,21 +316,21 @@ def generate_flowchart_html():
199
  def process_files(uploaded_files):
200
  """
201
  Process uploaded CSV files, generate usage plots, detect anomalies, and process AMC expiries.
202
- Returns a dataframe, plot path, PDF path, AMC expiry message, summary, and flowchart HTML.
203
  """
204
  # Log received files
205
  logging.info(f"Received uploaded files: {uploaded_files}")
206
 
207
  if not uploaded_files:
208
  logging.warning("No files uploaded.")
209
- return None, None, None, "Please upload at least one valid CSV file.", "## Summary\nNo files uploaded.", ""
210
 
211
  valid_files = [f for f in uploaded_files if f.name.endswith('.csv')]
212
  logging.info(f"Processing {len(valid_files)} valid files: {valid_files}")
213
 
214
  if not valid_files:
215
  logging.warning("No valid CSV files uploaded.")
216
- return None, None, None, "Please upload at least one valid CSV file.", "## Summary\nNo valid CSV files uploaded.", ""
217
 
218
  logging.info("Loading logs from uploaded files...")
219
  all_data = []
@@ -227,15 +344,15 @@ def process_files(uploaded_files):
227
  is_valid, error_msg = validate_csv(df)
228
  if not is_valid:
229
  logging.error(f"Failed to load {file.name}: {error_msg}")
230
- return None, None, None, f"Error loading {file.name}: {error_msg}", f"## Summary\nError: {error_msg}", ""
231
  all_data.append(df)
232
  except Exception as e:
233
  logging.error(f"Failed to load {file.name}: {str(e)}")
234
- return None, None, None, f"Error loading {file.name}: {str(e)}", f"## Summary\nError: {str(e)}", ""
235
 
236
  if not all_data:
237
  logging.warning("No data loaded from uploaded files.")
238
- return None, None, None, "No valid data found in uploaded files.", "## Summary\nNo data loaded.", ""
239
 
240
  combined_df = pd.concat(all_data, ignore_index=True)
241
  logging.info(f"Combined {len(combined_df)} total records.")
@@ -248,7 +365,7 @@ def process_files(uploaded_files):
248
  logging.info("Usage plot generated successfully.")
249
  else:
250
  logging.error("Failed to generate usage plot.")
251
- return combined_df, None, None, "Failed to generate usage plot.", "## Summary\nUsage plot generation failed.", ""
252
 
253
  # Detect anomalies using Local Outlier Factor
254
  logging.info("Detecting anomalies using Local Outlier Factor...")
@@ -275,6 +392,11 @@ def process_files(uploaded_files):
275
  summary = generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path)
276
  logging.info("Summary generated successfully.")
277
 
 
 
 
 
 
278
  # Generate flowchart HTML
279
  logging.info("Generating flowchart HTML...")
280
  flowchart_html = generate_flowchart_html()
@@ -285,7 +407,7 @@ def process_files(uploaded_files):
285
  if anomaly_df is not None:
286
  output_df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Unusual"})
287
 
288
- return output_df, plot_path, pdf_path, amc_message, summary, flowchart_html
289
 
290
  def generate_usage_plot(df):
291
  """
@@ -295,18 +417,18 @@ def generate_usage_plot(df):
295
  try:
296
  plt.figure(figsize=(12, 6))
297
  # Define colors for statuses
298
- status_colors = {'Active': '#36A2EB', 'Inactive': '#FF6384', 'Down': '#FFCE56', 'Online': '#4BC0C0'}
299
  for status in df['status'].unique():
300
  subset = df[df['status'] == status]
301
  plt.bar(
302
  subset['equipment'] + f" ({status})",
303
  subset['usage_count'],
304
  label=status,
305
- color=status_colors.get(status, '#999999')
306
  )
307
  plt.xlabel("Equipment (Status)", fontsize=12)
308
  plt.ylabel("Usage Count", fontsize=12)
309
- plt.title("Usage Count by Equipment and Status", fontsize=14)
310
  plt.legend(title="Status")
311
  plt.xticks(rotation=45, ha='right')
312
  plt.tight_layout()
@@ -373,7 +495,7 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
373
  def draw_header():
374
  c.setFont("Helvetica-Bold", 16)
375
  c.setFillColor(colors.darkblue)
376
- c.drawString(50, height - 50, "Equipment Log Analysis Report")
377
  c.setFont("Helvetica", 10)
378
  c.setFillColor(colors.black)
379
  c.drawString(50, height - 70, f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
@@ -398,7 +520,7 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
398
  c.drawString(50, y, f"Unique Devices: {', '.join(original_df['equipment'].unique())}")
399
  y -= 40
400
 
401
- # Data Table
402
  y = draw_section_title("Device Log Details", y)
403
  c.setFont("Helvetica-Bold", 10)
404
  headers = ["Equipment", "Usage Count", "Status", "AMC Expiry", "Activity"]
@@ -425,12 +547,12 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
425
  draw_header()
426
  c.setFont("Helvetica", 10)
427
 
428
- # Anomalies
429
- y = draw_section_title("Unusual Activity (Using Local Outlier Factor)", y)
430
  c.setFont("Helvetica", 12)
431
  if anomaly_df is not None:
432
  num_anomalies = sum(anomaly_df['anomaly'] == -1)
433
- c.drawString(50, y, f"Unusual Activities Detected: {num_anomalies}")
434
  y -= 20
435
  if num_anomalies > 0:
436
  anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
@@ -448,7 +570,7 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
448
  draw_header()
449
  c.setFont("Helvetica-Oblique", 10)
450
  else:
451
- c.drawString(50, y, "Unable to detect unusual activity due to an error.")
452
  y -= 20
453
  y -= 20
454
 
@@ -499,9 +621,9 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
499
  ("1. Upload CSV File(s)", "User uploads log files in CSV format containing device usage data."),
500
  ("2. Validate Data", "Ensures all required columns (equipment, usage_count, status, amc_expiry) are present and data types are correct (e.g., usage_count as numeric, amc_expiry as date)."),
501
  ("3. Generate Usage Chart", "Creates a bar chart showing usage counts by device and status (e.g., Active, Inactive) to visualize usage patterns."),
502
- ("4. Detect Unusual Activity", "Uses Local Outlier Factor (LOF) algorithm to identify devices with unusual usage patterns by comparing local density of usage counts (contamination=0.1, n_neighbors=5)."),
503
  ("5. Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05, calculating days left and urgency (urgent if ≤3 days)."),
504
- ("6. Create PDF Report", "Generates this PDF with a data table, unusual activity details, maintenance alerts, and this detailed flowchart.")
505
  ]
506
  for step, description in flowchart:
507
  c.drawString(50, y, step)
@@ -525,29 +647,30 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
525
 
526
  # Gradio interface
527
  with gr.Blocks(css=css) as demo:
528
- gr.Markdown("# Equipment Log Analysis")
529
- with gr.Row():
530
- file_input = gr.File(file_count="multiple", label="Upload CSV Files")
531
- process_button = gr.Button("Process Files")
532
  with gr.Row():
533
- output_summary = gr.Markdown(label="Summary of Results", elem_classes=["summary-card"])
 
534
  with gr.Row():
535
- output_df = gr.Dataframe(label="Processed Data")
536
- output_plot = gr.Image(label="Usage Chart")
537
- with gr.Row():
538
- output_message = gr.Textbox(label="Maintenance Alerts", elem_classes=["maintenance-alert"])
539
- output_pdf = gr.File(label="Download Detailed PDF Report")
540
- with gr.Row():
541
- output_flowchart = gr.HTML(generate_flowchart_html(), label="Processing Flowchart")
542
- with gr.Row():
543
- gr.Markdown("## Report Previews", elem_classes=["report-preview"])
544
- gr.Markdown("- **Usage Chart**: See the bar chart above for a visual of device usage by status.")
545
- gr.Markdown("- **PDF Report**: Download the PDF above for a full analysis, including data tables, unusual activity, maintenance alerts, and a detailed flowchart.")
546
-
 
 
 
547
  process_button.click(
548
  fn=process_files,
549
  inputs=[file_input],
550
- outputs=[output_df, output_plot, output_pdf, output_message, output_summary, output_flowchart]
551
  )
552
 
553
  if __name__ == "__main__":
 
15
 
16
  # CSS styling for the Gradio interface
17
  css = """
18
+ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
19
+ @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
20
+
21
  body {
22
+ font-family: 'Roboto', sans-serif;
23
+ background-color: #F9FAFB;
24
+ color: #1E40AF;
25
+ margin: 0;
26
+ padding: 20px;
27
  }
28
 
29
  h1 {
30
+ color: #1E40AF;
31
  text-align: center;
32
+ font-size: 2rem;
33
+ margin-bottom: 30px;
34
  }
35
 
36
  .gr-button {
37
+ background-color: #1E40AF;
38
  color: white;
39
  border: none;
40
+ border-radius: 8px;
41
+ padding: 12px 24px;
42
+ font-weight: 500;
43
+ transition: background-color 0.3s;
44
  }
45
 
46
  .gr-button:hover {
47
+ background-color: #3B82F6;
48
  }
49
 
50
+ .dashboard-container {
51
+ display: grid;
52
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
53
+ gap: 20px;
54
+ max-width: 1200px;
55
+ margin: 0 auto;
56
+ }
57
+
58
+ .card {
59
  background-color: white;
60
  border-radius: 10px;
61
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
62
  padding: 20px;
63
+ transition: transform 0.2s;
64
+ }
65
+
66
+ .card:hover {
67
+ transform: translateY(-5px);
68
  }
69
 
70
+ .card h2 {
71
+ color: #1E40AF;
72
+ font-size: 1.2rem;
73
  margin-top: 0;
74
+ margin-bottom: 15px;
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
  }
79
 
80
+ .device-card {
81
+ background-color: #EFF6FF;
82
+ border-left: 5px solid #1E40AF;
 
 
 
83
  }
84
 
85
+ .alert-card {
86
+ border-left: 5px solid #EF4444;
 
87
  }
88
 
89
+ .chart-container {
90
+ overflow-x: auto;
 
91
  }
92
 
93
+ .dataframe-container {
94
+ max-height: 400px;
95
+ overflow-y: auto;
96
+ }
97
+
98
+ .flowchart-container {
99
+ max-height: 400px;
100
+ overflow-y: auto;
101
  }
102
 
103
  .flowchart {
104
  display: flex;
105
  flex-direction: column;
106
  gap: 10px;
 
107
  }
108
 
109
  .flowchart-step {
110
+ background-color: #EFF6FF;
111
+ border-left: 5px solid #1E40AF;
112
+ padding: 15px;
113
  border-radius: 5px;
114
  position: relative;
115
  }
 
121
  left: 50%;
122
  transform: translateX(-50%);
123
  font-size: 20px;
124
+ color: #1E40AF;
125
  }
126
 
127
+ .alert-urgent {
128
+ color: #EF4444;
129
+ font-weight: bold;
130
+ }
131
+
132
+ .alert-upcoming {
133
+ color: #F59E0B;
134
+ font-weight: bold;
135
+ }
136
+
137
+ .recommendation {
138
+ font-style: italic;
139
+ color: #4B5563;
140
+ margin-top: 10px;
141
+ }
142
+
143
+ .anomaly-badge {
144
+ display: inline-block;
145
+ padding: 5px 10px;
146
+ border-radius: 12px;
147
+ font-size: 0.9rem;
148
+ font-weight: 500;
149
+ }
150
+
151
+ .anomaly-unusual {
152
+ background-color: #FEE2E2;
153
+ color: #EF4444;
154
+ }
155
+
156
+ .anomaly-normal {
157
+ background-color: #D1FAE5;
158
+ color: #10B981;
159
+ }
160
+
161
+ .download-button {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ background-color: #1E40AF;
166
+ color: white;
167
+ padding: 10px 20px;
168
+ border-radius: 8px;
169
+ text-decoration: none;
170
+ font-weight: 500;
171
+ transition: background-color 0.3s;
172
+ }
173
+
174
+ .download-button:hover {
175
+ background-color: #3B82F6;
176
+ }
177
+
178
+ /* Responsive Design */
179
+ @media (max-width: 768px) {
180
+ .dashboard-container {
181
+ grid-template-columns: 1fr;
182
+ }
183
+
184
+ h1 {
185
+ font-size: 1.5rem;
186
+ }
187
+
188
+ .card {
189
+ padding: 15px;
190
+ }
191
+
192
+ .gr-button {
193
+ width: 100%;
194
+ padding: 10px;
195
+ }
196
+
197
+ .download-button {
198
+ width: 100%;
199
+ justify-content: center;
200
+ }
201
  }
202
  """
203
 
 
218
  return False, f"Invalid data types: {str(e)}"
219
  return True, ""
220
 
221
+ def generate_device_cards(df, anomaly_df):
222
+ """
223
+ Generate HTML for device cards showing health, usage count, and status.
224
+ Returns an HTML string.
225
+ """
226
+ if anomaly_df is not None:
227
+ df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Unusual"})
228
+ else:
229
+ df['anomaly'] = "Unknown"
230
+
231
+ html = []
232
+ for equipment in df['equipment'].unique():
233
+ device_data = df[df['equipment'] == equipment].iloc[-1] # Latest record
234
+ anomaly_class = "anomaly-unusual" if device_data['anomaly'] == "Unusual" else "anomaly-normal"
235
+ html.append(f"""
236
+ <div class="card device-card">
237
+ <h2><i class="fas fa-microchip"></i> {equipment}</h2>
238
+ <p><strong>Status:</strong> {device_data['status']}</p>
239
+ <p><strong>Usage Count:</strong> {device_data['usage_count']}</p>
240
+ <p><strong>Activity:</strong> <span class="anomaly-badge {anomaly_class}">{device_data['anomaly']}</span></p>
241
+ <p><strong>AMC Expiry:</strong> {device_data['amc_expiry'].strftime('%Y-%m-%d')}</p>
242
+ </div>
243
+ """)
244
+ return "\n".join(html)
245
+
246
  def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
247
  """
248
  Generate a detailed and easy-to-understand summary of the processing results.
 
255
  total_records = len(combined_df)
256
  unique_devices = combined_df['equipment'].unique()
257
  summary.append(f"We processed **{total_records} log entries** for **{len(unique_devices)} devices** ({', '.join(unique_devices)}).")
258
+ summary.append("This dashboard provides real-time insights into device health, usage patterns, and maintenance needs.\n")
259
 
260
+ # Downtime Insights (Anomalies)
261
+ summary.append("## Downtime Insights")
262
  if anomaly_df is not None:
263
  num_anomalies = sum(anomaly_df['anomaly'] == -1)
264
  if num_anomalies > 0:
265
+ summary.append(f"**{num_anomalies} potential downtime risks** detected:")
266
  anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
267
  for _, row in anomaly_records.iterrows():
268
+ summary.append(f"- **{row['equipment']}** (Usage: {row['usage_count']}, Status: {row['status']}) - Indicates possible overuse or underuse.")
269
  else:
270
+ summary.append("No potential downtime risks detected. All devices are operating within expected patterns.")
271
  else:
272
+ summary.append("Unable to detect downtime risks due to an error.")
273
  summary.append("\n")
274
 
275
  # Maintenance Alerts (AMC Expiries)
 
290
  # Generated Reports
291
  summary.append("## Generated Reports")
292
  summary.append("- **Usage Chart**: Visualizes usage patterns across devices, helping identify overworked or underused equipment. See below for the chart.")
293
+ summary.append("- **PDF Report**: A comprehensive report including device logs, downtime insights, maintenance alerts, and a processing flowchart. Download it below.")
294
 
295
  return "\n".join(summary)
296
 
 
303
  ("Upload CSV File(s)", "User uploads log files in CSV format."),
304
  ("Validate Data", "Checks for required columns (equipment, usage_count, status, amc_expiry) and correct data types."),
305
  ("Generate Usage Chart", "Creates a bar chart showing usage counts by device and status (e.g., Active, Inactive)."),
306
+ ("Detect Downtime Risks", "Uses Local Outlier Factor to identify devices with unusual usage patterns (e.g., too high or too low)."),
307
  ("Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05."),
308
  ("Create PDF Report", "Generates a detailed PDF with data tables, insights, and this flowchart.")
309
  ]
 
316
  def process_files(uploaded_files):
317
  """
318
  Process uploaded CSV files, generate usage plots, detect anomalies, and process AMC expiries.
319
+ Returns a dataframe, plot path, PDF path, AMC expiry message, summary, device cards HTML, and flowchart HTML.
320
  """
321
  # Log received files
322
  logging.info(f"Received uploaded files: {uploaded_files}")
323
 
324
  if not uploaded_files:
325
  logging.warning("No files uploaded.")
326
+ return None, None, None, "Please upload at least one valid CSV file.", "## Summary\nNo files uploaded.", "", ""
327
 
328
  valid_files = [f for f in uploaded_files if f.name.endswith('.csv')]
329
  logging.info(f"Processing {len(valid_files)} valid files: {valid_files}")
330
 
331
  if not valid_files:
332
  logging.warning("No valid CSV files uploaded.")
333
+ return None, None, None, "Please upload at least one valid CSV file.", "## Summary\nNo valid CSV files uploaded.", "", ""
334
 
335
  logging.info("Loading logs from uploaded files...")
336
  all_data = []
 
344
  is_valid, error_msg = validate_csv(df)
345
  if not is_valid:
346
  logging.error(f"Failed to load {file.name}: {error_msg}")
347
+ return None, None, None, f"Error loading {file.name}: {error_msg}", f"## Summary\nError: {error_msg}", "", ""
348
  all_data.append(df)
349
  except Exception as e:
350
  logging.error(f"Failed to load {file.name}: {str(e)}")
351
+ return None, None, None, f"Error loading {file.name}: {str(e)}", f"## Summary\nError: {str(e)}", "", ""
352
 
353
  if not all_data:
354
  logging.warning("No data loaded from uploaded files.")
355
+ return None, None, None, "No valid data found in uploaded files.", "## Summary\nNo data loaded.", "", ""
356
 
357
  combined_df = pd.concat(all_data, ignore_index=True)
358
  logging.info(f"Combined {len(combined_df)} total records.")
 
365
  logging.info("Usage plot generated successfully.")
366
  else:
367
  logging.error("Failed to generate usage plot.")
368
+ return combined_df, None, None, "Failed to generate usage plot.", "## Summary\nUsage plot generation failed.", "", ""
369
 
370
  # Detect anomalies using Local Outlier Factor
371
  logging.info("Detecting anomalies using Local Outlier Factor...")
 
392
  summary = generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path)
393
  logging.info("Summary generated successfully.")
394
 
395
+ # Generate device cards
396
+ logging.info("Generating device cards HTML...")
397
+ device_cards_html = generate_device_cards(combined_df, anomaly_df)
398
+ logging.info("Device cards HTML generated successfully.")
399
+
400
  # Generate flowchart HTML
401
  logging.info("Generating flowchart HTML...")
402
  flowchart_html = generate_flowchart_html()
 
407
  if anomaly_df is not None:
408
  output_df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Unusual"})
409
 
410
+ return output_df, plot_path, pdf_path, amc_message, summary, device_cards_html, flowchart_html
411
 
412
  def generate_usage_plot(df):
413
  """
 
417
  try:
418
  plt.figure(figsize=(12, 6))
419
  # Define colors for statuses
420
+ status_colors = {'Active': '#3B82F6', 'Inactive': '#EF4444', 'Down': '#F59E0B', 'Online': '#10B981'}
421
  for status in df['status'].unique():
422
  subset = df[df['status'] == status]
423
  plt.bar(
424
  subset['equipment'] + f" ({status})",
425
  subset['usage_count'],
426
  label=status,
427
+ color=status_colors.get(status, '#6B7280')
428
  )
429
  plt.xlabel("Equipment (Status)", fontsize=12)
430
  plt.ylabel("Usage Count", fontsize=12)
431
+ plt.title("Device Usage Overview", fontsize=14, color='#1E40AF')
432
  plt.legend(title="Status")
433
  plt.xticks(rotation=45, ha='right')
434
  plt.tight_layout()
 
495
  def draw_header():
496
  c.setFont("Helvetica-Bold", 16)
497
  c.setFillColor(colors.darkblue)
498
+ c.drawString(50, height - 50, "Multi-Device LabOps Dashboard Report")
499
  c.setFont("Helvetica", 10)
500
  c.setFillColor(colors.black)
501
  c.drawString(50, height - 70, f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
 
520
  c.drawString(50, y, f"Unique Devices: {', '.join(original_df['equipment'].unique())}")
521
  y -= 40
522
 
523
+ # Device Log Details
524
  y = draw_section_title("Device Log Details", y)
525
  c.setFont("Helvetica-Bold", 10)
526
  headers = ["Equipment", "Usage Count", "Status", "AMC Expiry", "Activity"]
 
547
  draw_header()
548
  c.setFont("Helvetica", 10)
549
 
550
+ # Downtime Insights
551
+ y = draw_section_title("Downtime Insights (Using Local Outlier Factor)", y)
552
  c.setFont("Helvetica", 12)
553
  if anomaly_df is not None:
554
  num_anomalies = sum(anomaly_df['anomaly'] == -1)
555
+ c.drawString(50, y, f"Potential Downtime Risks Detected: {num_anomalies}")
556
  y -= 20
557
  if num_anomalies > 0:
558
  anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
 
570
  draw_header()
571
  c.setFont("Helvetica-Oblique", 10)
572
  else:
573
+ c.drawString(50, y, "Unable to detect downtime risks due to an error.")
574
  y -= 20
575
  y -= 20
576
 
 
621
  ("1. Upload CSV File(s)", "User uploads log files in CSV format containing device usage data."),
622
  ("2. Validate Data", "Ensures all required columns (equipment, usage_count, status, amc_expiry) are present and data types are correct (e.g., usage_count as numeric, amc_expiry as date)."),
623
  ("3. Generate Usage Chart", "Creates a bar chart showing usage counts by device and status (e.g., Active, Inactive) to visualize usage patterns."),
624
+ ("4. Detect Downtime Risks", "Uses Local Outlier Factor (LOF) algorithm to identify devices with unusual usage patterns by comparing local density of usage counts (contamination=0.1, n_neighbors=5)."),
625
  ("5. Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05, calculating days left and urgency (urgent if ≤3 days)."),
626
+ ("6. Create PDF Report", "Generates this PDF with a data table, downtime insights, maintenance alerts, and this detailed flowchart.")
627
  ]
628
  for step, description in flowchart:
629
  c.drawString(50, y, step)
 
647
 
648
  # Gradio interface
649
  with gr.Blocks(css=css) as demo:
650
+ gr.Markdown("# Multi-Device LabOps Dashboard")
 
 
 
651
  with gr.Row():
652
+ file_input = gr.File(file_count="multiple", label="Upload Device Logs (CSV)")
653
+ process_button = gr.Button("Process Logs")
654
  with gr.Row():
655
+ output_summary = gr.Markdown(label="Dashboard Summary", elem_classes=["card"])
656
+ with gr.Row(elem_classes=["dashboard-container"]):
657
+ output_device_cards = gr.HTML(label="Device Overview")
658
+ with gr.Row(elem_classes=["dashboard-container"]):
659
+ with gr.Column():
660
+ output_plot = gr.Image(label="Usage Chart", elem_classes=["card", "chart-container"])
661
+ with gr.Column():
662
+ output_message = gr.Textbox(label="Maintenance Alerts", elem_classes=["card", "alert-card"])
663
+ with gr.Row(elem_classes=["dashboard-container"]):
664
+ output_df = gr.Dataframe(label="Device Logs", elem_classes=["card", "dataframe-container"])
665
+ with gr.Row(elem_classes=["dashboard-container"]):
666
+ output_flowchart = gr.HTML(label="Processing Flowchart", elem_classes=["card", "flowchart-container"])
667
+ with gr.Row(elem_classes=["dashboard-container"]):
668
+ with gr.Column():
669
+ output_pdf = gr.File(label="Download Detailed Report", elem_classes=["card"])
670
  process_button.click(
671
  fn=process_files,
672
  inputs=[file_input],
673
+ outputs=[output_df, output_plot, output_pdf, output_message, output_summary, output_device_cards, output_flowchart]
674
  )
675
 
676
  if __name__ == "__main__":