Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -48,9 +48,9 @@ try:
|
|
| 48 |
"summarization",
|
| 49 |
model="t5-small",
|
| 50 |
device=device,
|
| 51 |
-
max_length=
|
| 52 |
min_length=10,
|
| 53 |
-
num_beams=
|
| 54 |
)
|
| 55 |
logging.info(f"Hugging Face model preloaded on {'GPU' if device == 0 else 'CPU'}")
|
| 56 |
except Exception as e:
|
|
@@ -164,12 +164,10 @@ def save_to_salesforce(df, reminders_df):
|
|
| 164 |
current_date = datetime.now()
|
| 165 |
next_30_days = current_date + timedelta(days=30)
|
| 166 |
records = []
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
df_to_save = df.head(max_records_to_save)
|
| 170 |
-
logging.info(f"Processing {len(df_to_save)} records for Salesforce (capped at {max_records_to_save})")
|
| 171 |
|
| 172 |
-
for idx, row in
|
| 173 |
status = str(row['status']).lower()
|
| 174 |
log_type = str(row['log_type']).lower()
|
| 175 |
status_mapped = picklist_mapping['Status__c'].get(status, status_values[0] if status_values else 'Active')
|
|
@@ -202,7 +200,7 @@ def save_to_salesforce(df, reminders_df):
|
|
| 202 |
records.append(record)
|
| 203 |
|
| 204 |
if records:
|
| 205 |
-
batch_size =
|
| 206 |
for i in range(0, len(records), batch_size):
|
| 207 |
batch = records[i:i + batch_size]
|
| 208 |
try:
|
|
@@ -222,12 +220,10 @@ def save_to_salesforce(df, reminders_df):
|
|
| 222 |
def summarize_logs(df):
|
| 223 |
start_time = time.time()
|
| 224 |
try:
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
total_devices = sample_df["device_id"].nunique()
|
| 228 |
-
most_used = sample_df.groupby("device_id")["usage_hours"].sum().idxmax() if not sample_df.empty else "N/A"
|
| 229 |
prompt = f"Maintenance logs: {total_devices} devices. Most used: {most_used}."
|
| 230 |
-
summary = summarizer(prompt, max_length=
|
| 231 |
logging.info(f"Summary generation took {time.time() - start_time:.2f} seconds")
|
| 232 |
return summary
|
| 233 |
except Exception as e:
|
|
@@ -278,12 +274,10 @@ def check_amc_reminders(df, current_date):
|
|
| 278 |
def generate_dashboard_insights(df):
|
| 279 |
start_time = time.time()
|
| 280 |
try:
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
total_devices = sample_df["device_id"].nunique()
|
| 284 |
-
avg_usage = sample_df["usage_hours"].mean() if "usage_hours" in sample_df.columns else 0
|
| 285 |
prompt = f"Insights: {total_devices} devices, avg usage {avg_usage:.2f} hours."
|
| 286 |
-
insights = summarizer(prompt, max_length=
|
| 287 |
logging.info(f"Insights generation took {time.time() - start_time:.2f} seconds")
|
| 288 |
return insights
|
| 289 |
except Exception as e:
|
|
@@ -296,13 +290,13 @@ def create_usage_chart(df):
|
|
| 296 |
if df.empty:
|
| 297 |
return None
|
| 298 |
usage_data = df.groupby("device_id")["usage_hours"].sum().reset_index()
|
| 299 |
-
if len(usage_data) >
|
| 300 |
-
usage_data = usage_data.nlargest(
|
| 301 |
fig = px.bar(
|
| 302 |
usage_data,
|
| 303 |
x="device_id",
|
| 304 |
y="usage_hours",
|
| 305 |
-
title="Usage Hours per Device
|
| 306 |
labels={"device_id": "Device ID", "usage_hours": "Usage Hours"}
|
| 307 |
)
|
| 308 |
fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
|
|
@@ -315,13 +309,13 @@ def create_usage_chart(df):
|
|
| 315 |
def create_downtime_chart(df):
|
| 316 |
try:
|
| 317 |
downtime_data = df.groupby("device_id")["downtime"].sum().reset_index()
|
| 318 |
-
if len(downtime_data) >
|
| 319 |
-
downtime_data = downtime_data.nlargest(
|
| 320 |
fig = px.bar(
|
| 321 |
downtime_data,
|
| 322 |
x="device_id",
|
| 323 |
y="downtime",
|
| 324 |
-
title="Downtime per Device
|
| 325 |
labels={"device_id": "Device ID", "downtime": "Downtime (Hours)"}
|
| 326 |
)
|
| 327 |
fig.update_layout(title_font_size=16, margin=dict(l=20, r=20, t=40, b=20))
|
|
@@ -348,6 +342,30 @@ def create_daily_log_trends_chart(df):
|
|
| 348 |
logging.error(f"Failed to create daily log trends chart: {str(e)}")
|
| 349 |
return None
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
# Create anomaly alerts chart
|
| 352 |
def create_anomaly_alerts_chart(anomalies_df):
|
| 353 |
try:
|
|
@@ -401,12 +419,32 @@ def generate_device_cards(df):
|
|
| 401 |
logging.error(f"Failed to generate device cards: {str(e)}")
|
| 402 |
return f'<p>Error generating device cards: {str(e)}</p>'
|
| 403 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
# Generate PDF content
|
| 405 |
-
def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards_html, daily_log_chart, anomaly_alerts_chart, downtime_chart, df):
|
| 406 |
if not reportlab_available:
|
| 407 |
return None
|
| 408 |
try:
|
| 409 |
-
pdf_path = f"
|
| 410 |
doc = SimpleDocTemplate(pdf_path, pagesize=letter)
|
| 411 |
styles = getSampleStyleSheet()
|
| 412 |
story = []
|
|
@@ -414,38 +452,38 @@ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights
|
|
| 414 |
def safe_paragraph(text, style):
|
| 415 |
return Paragraph(str(text).replace('\n', '<br/>'), style) if text else Paragraph("", style)
|
| 416 |
|
| 417 |
-
story.append(Paragraph("LabOps Status Report", styles['Title']))
|
| 418 |
story.append(Paragraph(f"Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", styles['Normal']))
|
| 419 |
story.append(Spacer(1, 12))
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
story.append(Paragraph("Summary Report", styles['Heading2']))
|
| 422 |
story.append(safe_paragraph(summary, styles['Normal']))
|
| 423 |
story.append(Spacer(1, 12))
|
| 424 |
|
| 425 |
story.append(Paragraph("Log Preview", styles['Heading2']))
|
| 426 |
if not preview_df.empty:
|
| 427 |
-
|
| 428 |
-
max_pdf_rows = 100
|
| 429 |
-
pdf_preview_df = preview_df.head(max_pdf_rows)
|
| 430 |
-
data = [pdf_preview_df.columns.tolist()] + pdf_preview_df.values.tolist()
|
| 431 |
table = Table(data)
|
| 432 |
table.setStyle(TableStyle([
|
| 433 |
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
| 434 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 435 |
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 436 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 437 |
-
('FONTSIZE', (0, 0), (-1, 0),
|
| 438 |
-
('BOTTOMPADDING', (0, 0), (-1, 0),
|
| 439 |
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
| 440 |
('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
|
| 441 |
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 442 |
-
('FONTSIZE', (0, 1), (-1, -1),
|
| 443 |
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
| 444 |
]))
|
| 445 |
story.append(table)
|
| 446 |
-
if len(preview_df) > max_pdf_rows:
|
| 447 |
-
story.append(Spacer(1, 12))
|
| 448 |
-
story.append(safe_paragraph(f"Note: Log Preview in PDF limited to {max_pdf_rows} rows. See dashboard for full table.", styles['Normal']))
|
| 449 |
else:
|
| 450 |
story.append(safe_paragraph("No preview available.", styles['Normal']))
|
| 451 |
story.append(Spacer(1, 12))
|
|
@@ -478,23 +516,21 @@ def generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights
|
|
| 478 |
return None
|
| 479 |
|
| 480 |
# Main processing function
|
| 481 |
-
async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_range, last_modified_state):
|
| 482 |
start_time = time.time()
|
| 483 |
try:
|
| 484 |
if not file_obj:
|
| 485 |
-
return "No file uploaded.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, "No anomalies detected.", "No AMC reminders.", "No insights generated.", None, last_modified_state
|
| 486 |
|
| 487 |
file_path = file_obj.name
|
| 488 |
current_modified_time = os.path.getmtime(file_path)
|
| 489 |
if last_modified_state and current_modified_time == last_modified_state:
|
| 490 |
-
return None, None, None, None, None, None, None, None, None, None, None, last_modified_state
|
| 491 |
|
| 492 |
logging.info(f"Processing file: {file_path}")
|
| 493 |
if not file_path.endswith(".csv"):
|
| 494 |
-
return "Please upload a CSV file.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, "", "", "", None, last_modified_state
|
| 495 |
|
| 496 |
-
# Cap the CSV size to avoid excessive processing
|
| 497 |
-
max_rows = 5000
|
| 498 |
required_columns = ["device_id", "log_type", "status", "timestamp", "usage_hours", "downtime", "amc_date"]
|
| 499 |
dtypes = {
|
| 500 |
"device_id": "string",
|
|
@@ -504,19 +540,17 @@ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_ra
|
|
| 504 |
"downtime": "float32",
|
| 505 |
"amc_date": "string"
|
| 506 |
}
|
| 507 |
-
df = pd.read_csv(file_path, dtype=dtypes
|
| 508 |
-
if len(df) == max_rows:
|
| 509 |
-
logging.warning(f"CSV exceeds {max_rows} rows. Only the first {max_rows} rows will be processed.")
|
| 510 |
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 511 |
if missing_columns:
|
| 512 |
-
return f"Missing columns: {missing_columns}", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, last_modified_state
|
| 513 |
|
| 514 |
df["timestamp"] = pd.to_datetime(df["timestamp"], errors='coerce')
|
| 515 |
df["amc_date"] = pd.to_datetime(df["amc_date"], errors='coerce')
|
| 516 |
if df["timestamp"].dt.tz is None:
|
| 517 |
df["timestamp"] = df["timestamp"].dt.tz_localize('UTC').dt.tz_convert('Asia/Kolkata')
|
| 518 |
if df.empty:
|
| 519 |
-
return "No data available.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, last_modified_state
|
| 520 |
|
| 521 |
# Apply filters
|
| 522 |
filtered_df = df.copy()
|
|
@@ -530,16 +564,22 @@ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_ra
|
|
| 530 |
start_date = today + pd.Timedelta(days=days_start)
|
| 531 |
end_date = today + pd.Timedelta(days=days_end) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
|
| 532 |
filtered_df = filtered_df[(filtered_df['timestamp'] >= start_date) & (filtered_df['timestamp'] <= end_date)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
if filtered_df.empty:
|
| 535 |
-
return "No data after applying filters.", pd.DataFrame(), None, '<p>No device cards available.</p>', None, None, None, None, None, None, None, last_modified_state
|
| 536 |
|
| 537 |
-
# Generate table for preview
|
| 538 |
-
preview_df = filtered_df
|
| 539 |
preview_html = preview_df.to_html(index=False, classes='table table-striped', border=0)
|
| 540 |
|
| 541 |
# Run tasks concurrently
|
| 542 |
-
with ThreadPoolExecutor(max_workers=
|
| 543 |
future_summary = executor.submit(summarize_logs, filtered_df)
|
| 544 |
future_anomalies = executor.submit(detect_anomalies, filtered_df)
|
| 545 |
future_amc = executor.submit(check_amc_reminders, filtered_df, datetime.now())
|
|
@@ -547,6 +587,7 @@ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_ra
|
|
| 547 |
future_usage_chart = executor.submit(create_usage_chart, filtered_df)
|
| 548 |
future_downtime_chart = executor.submit(create_downtime_chart, filtered_df)
|
| 549 |
future_daily_log_chart = executor.submit(create_daily_log_trends_chart, filtered_df)
|
|
|
|
| 550 |
future_device_cards = executor.submit(generate_device_cards, filtered_df)
|
| 551 |
future_reports = executor.submit(create_salesforce_reports, filtered_df)
|
| 552 |
|
|
@@ -559,39 +600,41 @@ async def process_logs(file_obj, lab_site_filter, equipment_type_filter, date_ra
|
|
| 559 |
usage_chart = future_usage_chart.result()
|
| 560 |
downtime_chart = future_downtime_chart.result()
|
| 561 |
daily_log_chart = future_daily_log_chart.result()
|
| 562 |
-
|
|
|
|
| 563 |
device_cards = future_device_cards.result()
|
| 564 |
|
| 565 |
save_to_salesforce(filtered_df, reminders_df)
|
| 566 |
-
pdf_file = generate_pdf_content(summary, preview_df, anomalies, amc_reminders, insights, device_cards, daily_log_chart, anomaly_alerts_chart, downtime_chart, filtered_df)
|
| 567 |
|
| 568 |
elapsed_time = time.time() - start_time
|
| 569 |
logging.info(f"Processing completed in {elapsed_time:.2f} seconds")
|
| 570 |
-
if elapsed_time >
|
| 571 |
-
logging.warning(f"Processing time exceeded
|
| 572 |
|
| 573 |
-
return (summary, preview_html, usage_chart, device_cards, daily_log_chart, anomaly_alerts_chart, downtime_chart, anomalies, amc_reminders, insights, pdf_file, current_modified_time)
|
| 574 |
except Exception as e:
|
| 575 |
logging.error(f"Failed to process file: {str(e)}")
|
| 576 |
-
return f"Error: {str(e)}", pd.DataFrame(), None, '<p>Error processing data.</p>', None, None, None, None, None, None, None, last_modified_state
|
| 577 |
|
| 578 |
# Update filters
|
| 579 |
def update_filters(file_obj):
|
| 580 |
if not file_obj:
|
| 581 |
-
return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All')
|
| 582 |
try:
|
| 583 |
with open(file_obj.name, 'rb') as f:
|
| 584 |
csv_content = f.read().decode('utf-8')
|
| 585 |
-
df = pd.read_csv(io.StringIO(csv_content)
|
| 586 |
df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
|
| 587 |
|
| 588 |
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']
|
| 589 |
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']
|
|
|
|
| 590 |
|
| 591 |
-
return gr.update(choices=lab_site_options, value='All'), gr.update(choices=equipment_type_options, value='All')
|
| 592 |
except Exception as e:
|
| 593 |
logging.error(f"Failed to update filters: {str(e)}")
|
| 594 |
-
return gr.update(choices=['All'], value='All'), gr.update(choices=['All'], value='All')
|
| 595 |
|
| 596 |
# Gradio Interface
|
| 597 |
try:
|
|
@@ -621,6 +664,7 @@ try:
|
|
| 621 |
lab_site_filter = gr.Dropdown(label="Lab Site", choices=['All'], value='All', interactive=True)
|
| 622 |
equipment_type_filter = gr.Dropdown(label="Equipment Type", choices=['All'], value='All', interactive=True)
|
| 623 |
date_range_filter = gr.Slider(label="Date Range (Days from Today)", minimum=-365, maximum=0, step=1, value=[-30, 0])
|
|
|
|
| 624 |
submit_button = gr.Button("Analyze", variant="primary")
|
| 625 |
|
| 626 |
with gr.Column(scale=2):
|
|
@@ -643,6 +687,8 @@ try:
|
|
| 643 |
downtime_chart_output = gr.Plot()
|
| 644 |
with gr.Tab("Daily Log Trends"):
|
| 645 |
daily_log_trends_output = gr.Plot()
|
|
|
|
|
|
|
| 646 |
with gr.Tab("Anomaly Alerts"):
|
| 647 |
anomaly_alerts_output = gr.Plot()
|
| 648 |
with gr.Group(elem_classes="dashboard-section"):
|
|
@@ -656,19 +702,19 @@ try:
|
|
| 656 |
insights_output = gr.Markdown()
|
| 657 |
with gr.Group(elem_classes="dashboard-section"):
|
| 658 |
gr.Markdown("### Export Report")
|
| 659 |
-
pdf_output = gr.File(label="Download Status Report as PDF")
|
| 660 |
|
| 661 |
file_input.change(
|
| 662 |
fn=update_filters,
|
| 663 |
inputs=[file_input],
|
| 664 |
-
outputs=[lab_site_filter, equipment_type_filter],
|
| 665 |
queue=False
|
| 666 |
)
|
| 667 |
|
| 668 |
submit_button.click(
|
| 669 |
fn=process_logs,
|
| 670 |
-
inputs=[file_input, lab_site_filter, equipment_type_filter, date_range_filter, last_modified_state],
|
| 671 |
-
outputs=[summary_output, preview_output, usage_chart_output, device_cards_output, daily_log_trends_output, anomaly_alerts_output, downtime_chart_output, anomaly_output, amc_output, insights_output, pdf_output, last_modified_state]
|
| 672 |
)
|
| 673 |
|
| 674 |
logging.info("Gradio interface initialized successfully")
|
|
|
|
| 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:
|
|
|
|
| 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')
|
|
|
|
| 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:
|
|
|
|
| 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:
|
|
|
|
| 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:
|
|
|
|
| 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))
|
|
|
|
| 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))
|
|
|
|
| 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:
|
|
|
|
| 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 |
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))
|
|
|
|
| 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",
|
|
|
|
| 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()
|
|
|
|
| 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())
|
|
|
|
| 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 |
|
|
|
|
| 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:
|
|
|
|
| 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):
|
|
|
|
| 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"):
|
|
|
|
| 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")
|