Update app.py
Browse files
app.py
CHANGED
|
@@ -13,7 +13,7 @@ import tempfile
|
|
| 13 |
# Configure logging to match the log format
|
| 14 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s,%(msecs)03d - %(levelname)s - %(message)s')
|
| 15 |
|
| 16 |
-
# CSS styling for the Gradio interface with a dark theme and
|
| 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');
|
|
@@ -34,7 +34,7 @@ h1 {
|
|
| 34 |
}
|
| 35 |
|
| 36 |
.gr-button {
|
| 37 |
-
background-color: #
|
| 38 |
color: #1F2937;
|
| 39 |
border: none;
|
| 40 |
border-radius: 8px;
|
|
@@ -44,7 +44,7 @@ h1 {
|
|
| 44 |
}
|
| 45 |
|
| 46 |
.gr-button:hover {
|
| 47 |
-
background-color: #
|
| 48 |
}
|
| 49 |
|
| 50 |
.dashboard-container {
|
|
@@ -226,21 +226,24 @@ def validate_csv(df):
|
|
| 226 |
Validate that the CSV has the required columns.
|
| 227 |
Returns True if valid, False otherwise with an error message.
|
| 228 |
"""
|
| 229 |
-
required_columns = ['
|
| 230 |
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 231 |
if missing_columns:
|
| 232 |
return False, f"Missing required columns: {', '.join(missing_columns)}"
|
| 233 |
# Validate data types
|
| 234 |
try:
|
| 235 |
-
df['
|
| 236 |
-
df['
|
|
|
|
|
|
|
|
|
|
| 237 |
except Exception as e:
|
| 238 |
return False, f"Invalid data types: {str(e)}"
|
| 239 |
return True, ""
|
| 240 |
|
| 241 |
def generate_device_cards(df, anomaly_df):
|
| 242 |
"""
|
| 243 |
-
Generate HTML for device cards showing health, usage
|
| 244 |
Returns an HTML string.
|
| 245 |
"""
|
| 246 |
if anomaly_df is not None:
|
|
@@ -249,15 +252,21 @@ def generate_device_cards(df, anomaly_df):
|
|
| 249 |
df['anomaly'] = "Unknown"
|
| 250 |
|
| 251 |
html = []
|
| 252 |
-
for
|
| 253 |
-
device_data = df[df['equipment'] ==
|
| 254 |
anomaly_class = "anomaly-unusual" if device_data['anomaly'] == "Unusual" else "anomaly-normal"
|
|
|
|
|
|
|
|
|
|
| 255 |
html.append(f"""
|
| 256 |
<div class="card device-card">
|
| 257 |
-
<h2><i class="fas fa-microchip"></i> {
|
| 258 |
<p><strong>Status:</strong> {device_data['status']}</p>
|
| 259 |
-
<p><strong>Usage
|
|
|
|
| 260 |
<p><strong>Activity:</strong> <span class="anomaly-badge {anomaly_class}">{device_data['anomaly']}</span></p>
|
|
|
|
|
|
|
| 261 |
<p><strong>AMC Expiry:</strong> {device_data['amc_expiry'].strftime('%Y-%m-%d')}</p>
|
| 262 |
</div>
|
| 263 |
""")
|
|
@@ -265,7 +274,7 @@ def generate_device_cards(df, anomaly_df):
|
|
| 265 |
|
| 266 |
def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
|
| 267 |
"""
|
| 268 |
-
Generate a detailed and easy-to-understand summary of the processing results.
|
| 269 |
Returns a markdown string for display in the Gradio interface.
|
| 270 |
"""
|
| 271 |
summary = []
|
|
@@ -274,7 +283,9 @@ def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
|
|
| 274 |
summary.append("## Overview")
|
| 275 |
total_records = len(combined_df)
|
| 276 |
unique_devices = combined_df['equipment'].unique()
|
|
|
|
| 277 |
summary.append(f"We processed **{total_records} log entries** for **{len(unique_devices)} devices** ({', '.join(unique_devices)}).")
|
|
|
|
| 278 |
summary.append("This dashboard provides real-time insights into device health, usage patterns, and maintenance needs.\n")
|
| 279 |
|
| 280 |
# Downtime Insights (Anomalies)
|
|
@@ -283,9 +294,10 @@ def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
|
|
| 283 |
num_anomalies = sum(anomaly_df['anomaly'] == -1)
|
| 284 |
if num_anomalies > 0:
|
| 285 |
summary.append(f"**{num_anomalies} potential downtime risks** detected:")
|
| 286 |
-
anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
|
| 287 |
for _, row in anomaly_records.iterrows():
|
| 288 |
-
|
|
|
|
| 289 |
else:
|
| 290 |
summary.append("No potential downtime risks detected. All devices are operating within expected patterns.")
|
| 291 |
else:
|
|
@@ -321,8 +333,8 @@ def generate_flowchart_html():
|
|
| 321 |
"""
|
| 322 |
steps = [
|
| 323 |
("Upload CSV File(s)", "User uploads log files in CSV format."),
|
| 324 |
-
("Validate Data", "Checks for required columns (
|
| 325 |
-
("Generate Usage Chart", "Creates a bar chart showing usage
|
| 326 |
("Detect Downtime Risks", "Uses Local Outlier Factor to identify devices with unusual usage patterns (e.g., too high or too low)."),
|
| 327 |
("Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05."),
|
| 328 |
("Create PDF Report", "Generates a detailed PDF with data tables, insights, and this flowchart.")
|
|
@@ -360,6 +372,12 @@ def process_files(uploaded_files):
|
|
| 360 |
try:
|
| 361 |
df = pd.read_csv(file.name)
|
| 362 |
logging.info(f"Loaded {len(df)} records from {file.name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
# Validate CSV structure
|
| 364 |
is_valid, error_msg = validate_csv(df)
|
| 365 |
if not is_valid:
|
|
@@ -437,7 +455,7 @@ def generate_usage_plot(df):
|
|
| 437 |
try:
|
| 438 |
plt.figure(figsize=(12, 6))
|
| 439 |
# Define colors for statuses (adjusted for dark theme visibility)
|
| 440 |
-
status_colors = {'
|
| 441 |
for status in df['status'].unique():
|
| 442 |
subset = df[df['status'] == status]
|
| 443 |
plt.bar(
|
|
@@ -447,7 +465,7 @@ def generate_usage_plot(df):
|
|
| 447 |
color=status_colors.get(status, '#6B7280')
|
| 448 |
)
|
| 449 |
plt.xlabel("Equipment (Status)", fontsize=12, color='#D1D5DB')
|
| 450 |
-
plt.ylabel("Usage
|
| 451 |
plt.title("Device Usage Overview", fontsize=14, color='#FFFFFF')
|
| 452 |
plt.legend(title="Status")
|
| 453 |
plt.xticks(rotation=45, ha='right', color='#D1D5DB')
|
|
@@ -541,13 +559,16 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
|
|
| 541 |
c.drawString(50, y, f"Total Records: {len(original_df)}")
|
| 542 |
y -= 20
|
| 543 |
c.drawString(50, y, f"Unique Devices: {', '.join(original_df['equipment'].unique())}")
|
|
|
|
|
|
|
|
|
|
| 544 |
y -= 40
|
| 545 |
|
| 546 |
# Device Log Details
|
| 547 |
y = draw_section_title("Device Log Details", y)
|
| 548 |
c.setFont("Helvetica-Bold", 10)
|
| 549 |
-
headers = ["Equipment", "Usage
|
| 550 |
-
x_positions = [50,
|
| 551 |
for i, header in enumerate(headers):
|
| 552 |
c.drawString(x_positions[i], y, header)
|
| 553 |
c.line(50, y - 5, width - 50, y - 5)
|
|
@@ -559,10 +580,13 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
|
|
| 559 |
output_df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Unusual"})
|
| 560 |
for _, row in output_df.iterrows():
|
| 561 |
c.drawString(50, y, str(row['equipment']))
|
| 562 |
-
c.drawString(
|
| 563 |
-
c.drawString(
|
| 564 |
-
c.drawString(
|
| 565 |
-
c.drawString(
|
|
|
|
|
|
|
|
|
|
| 566 |
y -= 20
|
| 567 |
if y < 50:
|
| 568 |
c.showPage()
|
|
@@ -578,12 +602,13 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
|
|
| 578 |
c.drawString(50, y, f"Potential Downtime Risks Detected: {num_anomalies}")
|
| 579 |
y -= 20
|
| 580 |
if num_anomalies > 0:
|
| 581 |
-
anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status']]
|
| 582 |
c.drawString(50, y, "Details:")
|
| 583 |
y -= 20
|
| 584 |
c.setFont("Helvetica-Oblique", 10)
|
| 585 |
for _, row in anomaly_records.iterrows():
|
| 586 |
-
|
|
|
|
| 587 |
y -= 20
|
| 588 |
c.drawString(70, y, "Note: This device’s usage is significantly higher or lower than others, which may indicate overuse or underuse.")
|
| 589 |
y -= 20
|
|
@@ -642,8 +667,8 @@ def generate_pdf_report(original_df, anomaly_df, amc_df):
|
|
| 642 |
c.setFont("Helvetica", 10)
|
| 643 |
flowchart = [
|
| 644 |
("1. Upload CSV File(s)", "User uploads log files in CSV format containing device usage data."),
|
| 645 |
-
("2. Validate Data", "Ensures all required columns (
|
| 646 |
-
("3. Generate Usage Chart", "Creates a bar chart showing usage
|
| 647 |
("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)."),
|
| 648 |
("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)."),
|
| 649 |
("6. Create PDF Report", "Generates this PDF with a data table, downtime insights, maintenance alerts, and this detailed flowchart.")
|
|
|
|
| 13 |
# Configure logging to match the log format
|
| 14 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s,%(msecs)03d - %(levelname)s - %(message)s')
|
| 15 |
|
| 16 |
+
# CSS styling for the Gradio interface with a dark theme and blue button
|
| 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');
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
.gr-button {
|
| 37 |
+
background-color: #3B82F6;
|
| 38 |
color: #1F2937;
|
| 39 |
border: none;
|
| 40 |
border-radius: 8px;
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
.gr-button:hover {
|
| 47 |
+
background-color: #2563EB;
|
| 48 |
}
|
| 49 |
|
| 50 |
.dashboard-container {
|
|
|
|
| 226 |
Validate that the CSV has the required columns.
|
| 227 |
Returns True if valid, False otherwise with an error message.
|
| 228 |
"""
|
| 229 |
+
required_columns = ['device_id', 'usage_hours', 'amc_date', 'status']
|
| 230 |
missing_columns = [col for col in required_columns if col not in df.columns]
|
| 231 |
if missing_columns:
|
| 232 |
return False, f"Missing required columns: {', '.join(missing_columns)}"
|
| 233 |
# Validate data types
|
| 234 |
try:
|
| 235 |
+
df['usage_hours'] = pd.to_numeric(df['usage_hours'], errors='raise')
|
| 236 |
+
df['amc_date'] = pd.to_datetime(df['amc_date'], errors='raise')
|
| 237 |
+
# Handle 'downtime' if present
|
| 238 |
+
if 'downtime' in df.columns:
|
| 239 |
+
df['downtime'] = pd.to_numeric(df['downtime'], errors='raise')
|
| 240 |
except Exception as e:
|
| 241 |
return False, f"Invalid data types: {str(e)}"
|
| 242 |
return True, ""
|
| 243 |
|
| 244 |
def generate_device_cards(df, anomaly_df):
|
| 245 |
"""
|
| 246 |
+
Generate HTML for device cards showing health, usage hours, downtime, status, log type, and timestamp.
|
| 247 |
Returns an HTML string.
|
| 248 |
"""
|
| 249 |
if anomaly_df is not None:
|
|
|
|
| 252 |
df['anomaly'] = "Unknown"
|
| 253 |
|
| 254 |
html = []
|
| 255 |
+
for device_id in df['equipment'].unique():
|
| 256 |
+
device_data = df[df['equipment'] == device_id].iloc[-1] # Latest record
|
| 257 |
anomaly_class = "anomaly-unusual" if device_data['anomaly'] == "Unusual" else "anomaly-normal"
|
| 258 |
+
downtime = device_data.get('downtime', 'N/A')
|
| 259 |
+
log_type = device_data.get('log_type', 'N/A')
|
| 260 |
+
timestamp = device_data.get('timestamp', 'N/A')
|
| 261 |
html.append(f"""
|
| 262 |
<div class="card device-card">
|
| 263 |
+
<h2><i class="fas fa-microchip"></i> {device_id}</h2>
|
| 264 |
<p><strong>Status:</strong> {device_data['status']}</p>
|
| 265 |
+
<p><strong>Usage Hours:</strong> {device_data['usage_count']}</p>
|
| 266 |
+
<p><strong>Downtime (hrs):</strong> {downtime}</p>
|
| 267 |
<p><strong>Activity:</strong> <span class="anomaly-badge {anomaly_class}">{device_data['anomaly']}</span></p>
|
| 268 |
+
<p><strong>Log Type:</strong> {log_type}</p>
|
| 269 |
+
<p><strong>Last Log:</strong> {timestamp}</p>
|
| 270 |
<p><strong>AMC Expiry:</strong> {device_data['amc_expiry'].strftime('%Y-%m-%d')}</p>
|
| 271 |
</div>
|
| 272 |
""")
|
|
|
|
| 274 |
|
| 275 |
def generate_summary(combined_df, anomaly_df, amc_df, plot_path, pdf_path):
|
| 276 |
"""
|
| 277 |
+
Generate a detailed and easy-to-understand summary of the processing results, including downtime.
|
| 278 |
Returns a markdown string for display in the Gradio interface.
|
| 279 |
"""
|
| 280 |
summary = []
|
|
|
|
| 283 |
summary.append("## Overview")
|
| 284 |
total_records = len(combined_df)
|
| 285 |
unique_devices = combined_df['equipment'].unique()
|
| 286 |
+
total_downtime = combined_df['downtime'].sum() if 'downtime' in combined_df.columns else 0
|
| 287 |
summary.append(f"We processed **{total_records} log entries** for **{len(unique_devices)} devices** ({', '.join(unique_devices)}).")
|
| 288 |
+
summary.append(f"Total downtime recorded: **{total_downtime} hours**.")
|
| 289 |
summary.append("This dashboard provides real-time insights into device health, usage patterns, and maintenance needs.\n")
|
| 290 |
|
| 291 |
# Downtime Insights (Anomalies)
|
|
|
|
| 294 |
num_anomalies = sum(anomaly_df['anomaly'] == -1)
|
| 295 |
if num_anomalies > 0:
|
| 296 |
summary.append(f"**{num_anomalies} potential downtime risks** detected:")
|
| 297 |
+
anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status', 'downtime']]
|
| 298 |
for _, row in anomaly_records.iterrows():
|
| 299 |
+
downtime = row['downtime'] if 'downtime' in row else 'N/A'
|
| 300 |
+
summary.append(f"- **{row['equipment']}** (Usage: {row['usage_count']}, Status: {row['status']}, Downtime: {downtime} hrs) - Indicates possible overuse or underuse.")
|
| 301 |
else:
|
| 302 |
summary.append("No potential downtime risks detected. All devices are operating within expected patterns.")
|
| 303 |
else:
|
|
|
|
| 333 |
"""
|
| 334 |
steps = [
|
| 335 |
("Upload CSV File(s)", "User uploads log files in CSV format."),
|
| 336 |
+
("Validate Data", "Checks for required columns (device_id, usage_hours, amc_date, status) and correct data types."),
|
| 337 |
+
("Generate Usage Chart", "Creates a bar chart showing usage hours by device and status (e.g., ok, warning)."),
|
| 338 |
("Detect Downtime Risks", "Uses Local Outlier Factor to identify devices with unusual usage patterns (e.g., too high or too low)."),
|
| 339 |
("Check Maintenance Dates", "Identifies devices with AMC expiries within 7 days from 2025-06-05."),
|
| 340 |
("Create PDF Report", "Generates a detailed PDF with data tables, insights, and this flowchart.")
|
|
|
|
| 372 |
try:
|
| 373 |
df = pd.read_csv(file.name)
|
| 374 |
logging.info(f"Loaded {len(df)} records from {file.name}")
|
| 375 |
+
# Rename columns to match expected names
|
| 376 |
+
df = df.rename(columns={
|
| 377 |
+
'device_id': 'equipment',
|
| 378 |
+
'usage_hours': 'usage_count',
|
| 379 |
+
'amc_date': 'amc_expiry'
|
| 380 |
+
})
|
| 381 |
# Validate CSV structure
|
| 382 |
is_valid, error_msg = validate_csv(df)
|
| 383 |
if not is_valid:
|
|
|
|
| 455 |
try:
|
| 456 |
plt.figure(figsize=(12, 6))
|
| 457 |
# Define colors for statuses (adjusted for dark theme visibility)
|
| 458 |
+
status_colors = {'ok': '#2DD4BF', 'warning': '#F87171', 'normal': '#10B981', 'down': '#FBBF24'}
|
| 459 |
for status in df['status'].unique():
|
| 460 |
subset = df[df['status'] == status]
|
| 461 |
plt.bar(
|
|
|
|
| 465 |
color=status_colors.get(status, '#6B7280')
|
| 466 |
)
|
| 467 |
plt.xlabel("Equipment (Status)", fontsize=12, color='#D1D5DB')
|
| 468 |
+
plt.ylabel("Usage Hours", fontsize=12, color='#D1D5DB')
|
| 469 |
plt.title("Device Usage Overview", fontsize=14, color='#FFFFFF')
|
| 470 |
plt.legend(title="Status")
|
| 471 |
plt.xticks(rotation=45, ha='right', color='#D1D5DB')
|
|
|
|
| 559 |
c.drawString(50, y, f"Total Records: {len(original_df)}")
|
| 560 |
y -= 20
|
| 561 |
c.drawString(50, y, f"Unique Devices: {', '.join(original_df['equipment'].unique())}")
|
| 562 |
+
y -= 20
|
| 563 |
+
total_downtime = original_df['downtime'].sum() if 'downtime' in original_df.columns else 0
|
| 564 |
+
c.drawString(50, y, f"Total Downtime: {total_downtime} hours")
|
| 565 |
y -= 40
|
| 566 |
|
| 567 |
# Device Log Details
|
| 568 |
y = draw_section_title("Device Log Details", y)
|
| 569 |
c.setFont("Helvetica-Bold", 10)
|
| 570 |
+
headers = ["Equipment", "Timestamp", "Usage Hours", "Downtime (hrs)", "Status", "Log Type", "AMC Expiry", "Activity"]
|
| 571 |
+
x_positions = [50, 110, 190, 260, 320, 370, 430, 490]
|
| 572 |
for i, header in enumerate(headers):
|
| 573 |
c.drawString(x_positions[i], y, header)
|
| 574 |
c.line(50, y - 5, width - 50, y - 5)
|
|
|
|
| 580 |
output_df['anomaly'] = anomaly_df['anomaly'].map({1: "Normal", -1: "Unusual"})
|
| 581 |
for _, row in output_df.iterrows():
|
| 582 |
c.drawString(50, y, str(row['equipment']))
|
| 583 |
+
c.drawString(110, y, str(row.get('timestamp', 'N/A')))
|
| 584 |
+
c.drawString(190, y, str(row['usage_count']))
|
| 585 |
+
c.drawString(260, y, str(row.get('downtime', 'N/A')))
|
| 586 |
+
c.drawString(320, y, str(row['status']))
|
| 587 |
+
c.drawString(370, y, str(row.get('log_type', 'N/A')))
|
| 588 |
+
c.drawString(430, y, str(row['amc_expiry'].strftime('%Y-%m-%d')))
|
| 589 |
+
c.drawString(490, y, str(row['anomaly']))
|
| 590 |
y -= 20
|
| 591 |
if y < 50:
|
| 592 |
c.showPage()
|
|
|
|
| 602 |
c.drawString(50, y, f"Potential Downtime Risks Detected: {num_anomalies}")
|
| 603 |
y -= 20
|
| 604 |
if num_anomalies > 0:
|
| 605 |
+
anomaly_records = anomaly_df[anomaly_df['anomaly'] == -1][['equipment', 'usage_count', 'status', 'downtime']]
|
| 606 |
c.drawString(50, y, "Details:")
|
| 607 |
y -= 20
|
| 608 |
c.setFont("Helvetica-Oblique", 10)
|
| 609 |
for _, row in anomaly_records.iterrows():
|
| 610 |
+
downtime = row['downtime'] if 'downtime' in row else 'N/A'
|
| 611 |
+
c.drawString(50, y, f"{row['equipment']}: Usage Hours = {row['usage_count']}, Status = {row['status']}, Downtime = {downtime} hrs")
|
| 612 |
y -= 20
|
| 613 |
c.drawString(70, y, "Note: This device’s usage is significantly higher or lower than others, which may indicate overuse or underuse.")
|
| 614 |
y -= 20
|
|
|
|
| 667 |
c.setFont("Helvetica", 10)
|
| 668 |
flowchart = [
|
| 669 |
("1. Upload CSV File(s)", "User uploads log files in CSV format containing device usage data."),
|
| 670 |
+
("2. Validate Data", "Ensures all required columns (device_id, usage_hours, amc_date, status) are present and data types are correct (e.g., usage_hours as numeric, amc_date as date)."),
|
| 671 |
+
("3. Generate Usage Chart", "Creates a bar chart showing usage hours by device and status (e.g., ok, warning) to visualize usage patterns."),
|
| 672 |
("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)."),
|
| 673 |
("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)."),
|
| 674 |
("6. Create PDF Report", "Generates this PDF with a data table, downtime insights, maintenance alerts, and this detailed flowchart.")
|