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.")
|