pranit144 commited on
Commit
1c482cd
·
verified ·
1 Parent(s): 9a5d71c

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +30 -0
  2. README.md +10 -10
  3. Spacefile +7 -0
  4. app.py +431 -0
  5. requirements.txt +8 -0
  6. templates/index.html +745 -0
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory
5
+ WORKDIR /code
6
+
7
+ # Set environment variables
8
+ ENV PORT=7860
9
+
10
+ # Install system dependencies
11
+ RUN apt-get update && apt-get install -y \
12
+ gcc \
13
+ python3-dev \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Create necessary directories
17
+ RUN mkdir -p /code/uploads /code/logs
18
+
19
+ # Copy requirements and install dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy the application
24
+ COPY . .
25
+
26
+ # Make directories writable
27
+ RUN chmod -R 777 /code/uploads /code/logs
28
+
29
+ # Command to run the application
30
+ CMD gunicorn --bind 0.0.0.0:${PORT} --workers 1 --timeout 120 app:app
README.md CHANGED
@@ -1,10 +1,10 @@
1
- ---
2
- title: Anamloi Detection Using Ml
3
- emoji:
4
- colorFrom: yellow
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Predictive Maintenance Dashboard
2
+
3
+ A Flask-based dashboard for monitoring and analyzing maintenance data with interactive visualizations and predictive insights.
4
+
5
+ ## Features
6
+ - Real-time component monitoring
7
+ - Anomaly detection
8
+ - Predictive maintenance suggestions
9
+ - Interactive data visualization
10
+ - Component health analysis
Spacefile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Spacefile Docs: https://huggingface.co/docs/hub/spaces-config-reference
2
+ title: Predictive Maintenance Dashboard
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 7860
app.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, render_template, jsonify, redirect, url_for, flash
2
+ import pandas as pd
3
+ import plotly.graph_objects as go
4
+ import plotly.express as px
5
+ import json
6
+ import io
7
+ import base64
8
+ import os
9
+ import logging
10
+ from logging.handlers import RotatingFileHandler
11
+ from werkzeug.middleware.proxy_fix import ProxyFix
12
+
13
+ app = Flask(__name__)
14
+ app.secret_key = 'your_secret_key_here'
15
+
16
+ # Create upload folder and logs folder
17
+ UPLOAD_FOLDER = 'uploads'
18
+ LOGS_FOLDER = 'logs'
19
+ for folder in [UPLOAD_FOLDER, LOGS_FOLDER]:
20
+ if not os.path.exists(folder):
21
+ os.makedirs(folder)
22
+
23
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
24
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
25
+
26
+ # Setup logging
27
+ if not app.debug:
28
+ file_handler = RotatingFileHandler('logs/maintenance_dashboard.log', maxBytes=10240, backupCount=10)
29
+ file_handler.setFormatter(logging.Formatter(
30
+ '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
31
+ ))
32
+ file_handler.setLevel(logging.INFO)
33
+ app.logger.addHandler(file_handler)
34
+ app.logger.setLevel(logging.INFO)
35
+ app.logger.info('Maintenance Dashboard startup')
36
+
37
+ class DataStore:
38
+ def __init__(self):
39
+ self.uploaded_data = None
40
+ self.current_index = 0
41
+ self.processed_data = []
42
+ self.anomalies = []
43
+
44
+ data_store = DataStore()
45
+
46
+ def detect_anomalies(df):
47
+ """Checks each row of the dataframe for readings that exceed preset thresholds."""
48
+ alerts = []
49
+ thresholds = {
50
+ 'brakes': 90,
51
+ 'filters':90,
52
+ 'cables': 90
53
+ }
54
+
55
+ for idx, row in df.iterrows():
56
+ row_alerts = []
57
+ for component, threshold in thresholds.items():
58
+ if component in df.columns:
59
+ value = row[component]
60
+ if pd.notna(value) and value > threshold:
61
+ row_alerts.append(
62
+ f"{component.capitalize()} reading ({value}) exceeds threshold ({threshold})"
63
+ )
64
+ if row_alerts:
65
+ alerts.append({
66
+ 'row': idx + 1,
67
+ 'messages': row_alerts
68
+ })
69
+ return alerts
70
+
71
+ def analyze_component_trends(df):
72
+ """Analyze trends and patterns in component data"""
73
+ trends = {}
74
+ for component in ['brakes', 'filters', 'cables']:
75
+ # Calculate rolling average to identify trends
76
+ rolling_avg = df[component].rolling(window=3).mean()
77
+
78
+ # Calculate rate of change
79
+ rate_of_change = df[component].diff().mean()
80
+
81
+ # Identify peak usage periods
82
+ peak_threshold = df[component].quantile(0.75)
83
+ peak_periods = df[component] > peak_threshold
84
+
85
+ trends[component] = {
86
+ 'trend': 'Increasing' if rate_of_change > 1 else 'Decreasing' if rate_of_change < -1 else 'Stable',
87
+ 'rate_of_change': round(rate_of_change, 2),
88
+ 'peak_usage_frequency': round((peak_periods.sum() / len(df)) * 100, 1),
89
+ 'recent_trend': 'Up' if rolling_avg.iloc[-1] > rolling_avg.iloc[-2] else 'Down'
90
+ }
91
+ return trends
92
+
93
+ def generate_maintenance_insights(df, trends):
94
+ """Generate detailed maintenance insights based on data analysis"""
95
+ insights = {
96
+ 'critical_analysis': [],
97
+ 'maintenance_recommendations': [],
98
+ 'preventive_measures': [],
99
+ 'optimization_suggestions': []
100
+ }
101
+
102
+ for component in ['brakes', 'filters', 'cables']:
103
+ avg = df[component].mean()
104
+ max_val = df[component].max()
105
+ std_dev = df[component].std()
106
+ trend = trends[component]
107
+
108
+ # Critical Analysis
109
+ if avg > 70:
110
+ insights['critical_analysis'].append({
111
+ 'component': component,
112
+ 'severity': 'High' if avg > 80 else 'Medium',
113
+ 'reason': f"Sustained high readings (avg: {round(avg, 1)}%)",
114
+ 'trend': trend['trend'],
115
+ 'impact': 'Immediate attention required' if avg > 80 else 'Monitor closely'
116
+ })
117
+
118
+ # Maintenance Recommendations
119
+ if trend['trend'] == 'Increasing' and avg > 60:
120
+ insights['maintenance_recommendations'].append({
121
+ 'component': component,
122
+ 'urgency': 'High' if avg > 75 else 'Medium',
123
+ 'action': f"Schedule maintenance within {' 24 hours' if avg > 75 else ' one week'}",
124
+ 'reason': f"Increasing trend with high average ({round(avg, 1)}%)"
125
+ })
126
+
127
+ # Preventive Measures
128
+ if std_dev > 10 or trend['peak_usage_frequency'] > 30:
129
+ insights['preventive_measures'].append({
130
+ 'component': component,
131
+ 'measure': f"Implement regular checks for {component}",
132
+ 'frequency': 'Daily' if std_dev > 15 else 'Weekly',
133
+ 'reason': f"High variability (±{round(std_dev, 1)}%) and frequent peak usage ({trend['peak_usage_frequency']}% of time)"
134
+ })
135
+
136
+ # Optimization Suggestions
137
+ if trend['rate_of_change'] > 2 or max_val > 90:
138
+ insights['optimization_suggestions'].append({
139
+ 'component': component,
140
+ 'suggestion': f"Review {component} usage patterns",
141
+ 'potential_impact': 'High',
142
+ 'expected_benefit': 'Reduced wear and extended component life'
143
+ })
144
+
145
+ return insights
146
+
147
+ def calculate_statistics(df):
148
+ """Calculate important statistics from the data"""
149
+ stats = {
150
+ 'component_averages': {
151
+ 'brakes': round(df['brakes'].mean(), 2),
152
+ 'filters': round(df['filters'].mean(), 2),
153
+ 'cables': round(df['cables'].mean(), 2)
154
+ },
155
+ 'critical_components': [],
156
+ 'maintenance_suggestions': [],
157
+ 'component_health': {}, # New: Component health status
158
+ 'maintenance_priority': [], # New: Prioritized maintenance list
159
+ 'performance_metrics': {}, # New: Detailed performance metrics
160
+ 'detailed_analysis': {} # New: Detailed analysis
161
+ }
162
+
163
+ # Calculate component health and status
164
+ for component in ['brakes', 'filters', 'cables']:
165
+ avg = df[component].mean()
166
+ max_val = df[component].max()
167
+ min_val = df[component].min()
168
+ std_dev = df[component].std()
169
+
170
+ # Calculate health score (0-100)
171
+ health_score = max(0, min(100, 100 - (avg / 100 * 100)))
172
+
173
+ stats['component_health'][component] = {
174
+ 'health_score': round(health_score, 1),
175
+ 'average': round(avg, 2),
176
+ 'max_reading': round(max_val, 2),
177
+ 'min_reading': round(min_val, 2),
178
+ 'variability': round(std_dev, 2),
179
+ 'status': 'Good' if avg < 60 else 'Warning' if avg < 75 else 'Critical'
180
+ }
181
+
182
+ # Identify critical components
183
+ if avg > 70:
184
+ stats['critical_components'].append({
185
+ 'name': component,
186
+ 'avg_value': round(avg, 2),
187
+ 'max_value': round(max_val, 2),
188
+ 'health_score': round(health_score, 1),
189
+ 'status': 'Critical' if avg > 80 else 'Warning',
190
+ 'variability': round(std_dev, 2)
191
+ })
192
+
193
+ # Generate prioritized maintenance suggestions
194
+ for component, health in stats['component_health'].items():
195
+ if health['average'] > 80:
196
+ stats['maintenance_priority'].append({
197
+ 'component': component,
198
+ 'priority': 'High',
199
+ 'timeline': 'Immediate',
200
+ 'reason': f"Critical readings (Avg: {health['average']}%)",
201
+ 'recommendation': f"Schedule immediate maintenance for {component}"
202
+ })
203
+ elif health['average'] > 70:
204
+ stats['maintenance_priority'].append({
205
+ 'component': component,
206
+ 'priority': 'Medium',
207
+ 'timeline': 'Within 1 week',
208
+ 'reason': f"Warning levels (Avg: {health['average']}%)",
209
+ 'recommendation': f"Plan maintenance for {component} soon"
210
+ })
211
+ elif health['variability'] > 10:
212
+ stats['maintenance_priority'].append({
213
+ 'component': component,
214
+ 'priority': 'Low',
215
+ 'timeline': 'Monitor',
216
+ 'reason': f"High variability (±{health['variability']}%)",
217
+ 'recommendation': f"Monitor {component} performance"
218
+ })
219
+
220
+ # Generate detailed maintenance suggestions
221
+ for component, health in stats['component_health'].items():
222
+ suggestions = []
223
+
224
+ if health['average'] > 80:
225
+ suggestions.append(f"URGENT: Immediate maintenance required - {component} showing critical wear")
226
+ elif health['average'] > 70:
227
+ suggestions.append(f"WARNING: Schedule maintenance soon - {component} performance degrading")
228
+
229
+ if health['variability'] > 10:
230
+ suggestions.append(f"Monitor {component} - Showing inconsistent readings (±{health['variability']}%)")
231
+
232
+ if health['max_reading'] > 90:
233
+ suggestions.append(f"Investigate {component} peak readings of {health['max_reading']}%")
234
+
235
+ if suggestions:
236
+ stats['maintenance_suggestions'].extend(suggestions)
237
+
238
+ # Calculate performance metrics
239
+ stats['performance_metrics'] = {
240
+ 'overall_health': round(sum(h['health_score'] for h in stats['component_health'].values()) / 3, 1),
241
+ 'critical_count': len([h for h in stats['component_health'].values() if h['status'] == 'Critical']),
242
+ 'warning_count': len([h for h in stats['component_health'].values() if h['status'] == 'Warning']),
243
+ 'healthy_count': len([h for h in stats['component_health'].values() if h['status'] == 'Good'])
244
+ }
245
+
246
+ # Add new detailed analyses
247
+ trends = analyze_component_trends(df)
248
+ maintenance_insights = generate_maintenance_insights(df, trends)
249
+
250
+ stats['detailed_analysis'] = {
251
+ 'trends': trends,
252
+ 'insights': maintenance_insights
253
+ }
254
+
255
+ return stats
256
+
257
+ def create_graphs(df):
258
+ """Create all visualization graphs"""
259
+ graphs = {}
260
+
261
+ # Gauge Charts for all components
262
+ components = ['brakes', 'filters', 'cables']
263
+ for component in components:
264
+ latest_value = df[component].iloc[-1]
265
+ gauge = go.Figure(go.Indicator(
266
+ mode="gauge+number+delta",
267
+ value=latest_value,
268
+ delta={'reference': df[component].iloc[-2] if len(df) > 1 else latest_value},
269
+ domain={'x': [0, 1], 'y': [0, 1]},
270
+ title={'text': f"Latest {component.title()} Reading"},
271
+ gauge={
272
+ 'axis': {'range': [None, 100]},
273
+ 'bar': {'color': "#1f77b4"},
274
+ 'threshold': {
275
+ 'line': {'color': "red", 'width': 4},
276
+ 'thickness': 0.75,
277
+ 'value': 90
278
+ },
279
+ 'steps': [
280
+ {'range': [0, 60], 'color': "#3feb48"},
281
+ {'range': [60, 80], 'color': "#ebeb3f"},
282
+ {'range': [80, 100], 'color': "#eb3f3f"}
283
+ ]
284
+ }))
285
+ gauge.update_layout(height=300)
286
+ graphs[f'{component}_gauge'] = gauge.to_html(full_html=False)
287
+
288
+ # Bar Chart for current readings with historical average
289
+ current_values = [df[comp].iloc[-1] for comp in components]
290
+ avg_values = [df[comp].mean() for comp in components]
291
+
292
+ bar = go.Figure(data=[
293
+ go.Bar(name='Current Reading', x=components, y=current_values),
294
+ go.Bar(name='Historical Average', x=components, y=avg_values)
295
+ ])
296
+ bar.update_layout(
297
+ title="Component Readings Comparison",
298
+ barmode='group',
299
+ height=400
300
+ )
301
+ graphs['bar'] = bar.to_html(full_html=False)
302
+
303
+ # Time Series Chart with Moving Average
304
+ fig_time = go.Figure()
305
+ for component in components:
306
+ # Add raw data
307
+ fig_time.add_trace(go.Scatter(
308
+ y=df[component],
309
+ name=component.title(),
310
+ mode='lines'
311
+ ))
312
+ # Add moving average
313
+ ma = df[component].rolling(window=3).mean()
314
+ fig_time.add_trace(go.Scatter(
315
+ y=ma,
316
+ name=f"{component.title()} MA",
317
+ line=dict(dash='dash'),
318
+ opacity=0.5
319
+ ))
320
+
321
+ fig_time.update_layout(
322
+ title='Component Readings Over Time',
323
+ height=400,
324
+ showlegend=True,
325
+ legend=dict(
326
+ orientation="h",
327
+ yanchor="bottom",
328
+ y=1.02,
329
+ xanchor="right",
330
+ x=1
331
+ )
332
+ )
333
+ graphs['timeseries'] = fig_time.to_html(full_html=False)
334
+
335
+ # Correlation Matrix Heatmap
336
+ corr_matrix = df[components].corr()
337
+ heatmap = go.Figure(data=go.Heatmap(
338
+ z=corr_matrix,
339
+ x=components,
340
+ y=components,
341
+ colorscale='RdBu',
342
+ zmin=-1,
343
+ zmax=1
344
+ ))
345
+ heatmap.update_layout(
346
+ title='Component Correlation Matrix',
347
+ height=400
348
+ )
349
+ graphs['heatmap'] = heatmap.to_html(full_html=False)
350
+
351
+ # Box Plot for Distribution Analysis
352
+ box_data = [go.Box(y=df[component], name=component.title()) for component in components]
353
+ box_plot = go.Figure(data=box_data)
354
+ box_plot.update_layout(
355
+ title='Component Value Distributions',
356
+ height=400
357
+ )
358
+ graphs['box_plot'] = box_plot.to_html(full_html=False)
359
+
360
+ # Scatter Matrix
361
+ scatter_matrix = px.scatter_matrix(
362
+ df[components],
363
+ dimensions=components,
364
+ title='Component Relationships Matrix'
365
+ )
366
+ scatter_matrix.update_layout(height=600)
367
+ graphs['scatter_matrix'] = scatter_matrix.to_html(full_html=False)
368
+
369
+ return graphs
370
+
371
+ @app.route('/')
372
+ def index():
373
+ return render_template('index.html',
374
+ data=None,
375
+ graphs=None,
376
+ stats=None,
377
+ anomalies=None)
378
+
379
+ @app.route('/upload', methods=['POST'])
380
+ def upload():
381
+ if 'file' not in request.files:
382
+ flash('No file uploaded', 'error')
383
+ return redirect(url_for('index'))
384
+
385
+ file = request.files['file']
386
+ if file.filename == '':
387
+ flash('No file selected', 'error')
388
+ return redirect(url_for('index'))
389
+
390
+ try:
391
+ # Read the file
392
+ if file.filename.endswith('.csv'):
393
+ df = pd.read_csv(file)
394
+ elif file.filename.endswith(('.xlsx', '.xls')):
395
+ df = pd.read_excel(file)
396
+ else:
397
+ flash('Unsupported file format. Please upload CSV or Excel file.', 'error')
398
+ return redirect(url_for('index'))
399
+
400
+ # Validate required columns
401
+ required_columns = ['brakes', 'filters', 'cables']
402
+ missing_columns = [col for col in required_columns if col not in df.columns]
403
+
404
+ if missing_columns:
405
+ flash(f"Missing required columns: {', '.join(missing_columns)}", 'error')
406
+ return redirect(url_for('index'))
407
+
408
+ # Store the data and process it
409
+ data_store.uploaded_data = df
410
+ data_store.anomalies = detect_anomalies(df)
411
+
412
+ # Calculate statistics
413
+ stats = calculate_statistics(df)
414
+
415
+ # Create graphs
416
+ graphs = create_graphs(df)
417
+
418
+ # Render template with all the data
419
+ return render_template('index.html',
420
+ data=df.to_dict('records'),
421
+ graphs=graphs,
422
+ stats=stats,
423
+ anomalies=data_store.anomalies)
424
+
425
+ except Exception as e:
426
+ flash(f"Error processing file: {str(e)}", 'error')
427
+ return redirect(url_for('index'))
428
+
429
+ if __name__ == '__main__':
430
+ port = int(os.environ.get('PORT', 5000))
431
+ app.run(host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ pandas
3
+ plotly
4
+ numpy
5
+ openpyxl # For Excel file support
6
+ gunicorn # For production server
7
+ Werkzeug
8
+ python-dotenv
templates/index.html ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Predictive Maintenance Dashboard</title>
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --primary-color: #2563eb;
11
+ --secondary-color: #3b82f6;
12
+ --success-color: #10b981;
13
+ --warning-color: #f59e0b;
14
+ --danger-color: #ef4444;
15
+ --dark-color: #1f2937;
16
+ --light-color: #f3f4f6;
17
+ --border-color: #e5e7eb;
18
+ --text-primary: #111827;
19
+ --text-secondary: #4b5563;
20
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
21
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
22
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
23
+ }
24
+
25
+ * {
26
+ margin: 0;
27
+ padding: 0;
28
+ box-sizing: border-box;
29
+ font-family: 'Inter', sans-serif;
30
+ }
31
+
32
+ body {
33
+ background-color: #f8fafc;
34
+ color: var(--text-primary);
35
+ line-height: 1.5;
36
+ }
37
+
38
+ /* Dashboard Layout */
39
+ .dashboard-container {
40
+ display: flex;
41
+ min-height: 100vh;
42
+ }
43
+
44
+ /* Sidebar */
45
+ .sidebar {
46
+ width: 280px;
47
+ background: white;
48
+ border-right: 1px solid var(--border-color);
49
+ position: fixed;
50
+ height: 100vh;
51
+ overflow-y: auto;
52
+ }
53
+
54
+ .sidebar-header {
55
+ padding: 1.5rem;
56
+ border-bottom: 1px solid var(--border-color);
57
+ }
58
+
59
+ .sidebar-header h1 {
60
+ font-size: 1.25rem;
61
+ font-weight: 600;
62
+ color: var(--dark-color);
63
+ }
64
+
65
+ .sidebar-content {
66
+ padding: 1.5rem;
67
+ }
68
+
69
+ /* Main Content */
70
+ .main-content {
71
+ flex: 1;
72
+ margin-left: 280px;
73
+ padding: 2rem;
74
+ }
75
+
76
+ /* Upload Section */
77
+ .upload-container {
78
+ background: white;
79
+ border-radius: 12px;
80
+ padding: 2rem;
81
+ box-shadow: var(--shadow);
82
+ margin-bottom: 2rem;
83
+ }
84
+
85
+ .upload-form {
86
+ display: flex;
87
+ gap: 1rem;
88
+ align-items: center;
89
+ }
90
+
91
+ .file-input-wrapper {
92
+ flex: 1;
93
+ position: relative;
94
+ }
95
+
96
+ .file-input {
97
+ width: 100%;
98
+ padding: 0.75rem 1rem;
99
+ border: 2px dashed var(--border-color);
100
+ border-radius: 8px;
101
+ cursor: pointer;
102
+ transition: all 0.3s ease;
103
+ }
104
+
105
+ .file-input:hover {
106
+ border-color: var(--primary-color);
107
+ }
108
+
109
+ .upload-button {
110
+ background: var(--primary-color);
111
+ color: white;
112
+ padding: 0.75rem 1.5rem;
113
+ border: none;
114
+ border-radius: 8px;
115
+ font-weight: 500;
116
+ cursor: pointer;
117
+ transition: all 0.3s ease;
118
+ }
119
+
120
+ .upload-button:hover {
121
+ background: var(--secondary-color);
122
+ }
123
+
124
+ /* Dashboard Grid */
125
+ .dashboard-grid {
126
+ display: grid;
127
+ grid-template-columns: repeat(2, 1fr);
128
+ gap: 1.5rem;
129
+ margin-bottom: 2rem;
130
+ }
131
+
132
+ .graph-card {
133
+ background: white;
134
+ border-radius: 12px;
135
+ padding: 1.5rem;
136
+ box-shadow: var(--shadow);
137
+ }
138
+
139
+ .graph-card h3 {
140
+ font-size: 1.1rem;
141
+ font-weight: 600;
142
+ margin-bottom: 1rem;
143
+ color: var(--dark-color);
144
+ }
145
+
146
+ /* Insights Section */
147
+ .insights-container {
148
+ background: white;
149
+ border-radius: 12px;
150
+ padding: 2rem;
151
+ box-shadow: var(--shadow);
152
+ margin-bottom: 2rem;
153
+ }
154
+
155
+ .insights-grid {
156
+ display: grid;
157
+ grid-template-columns: repeat(2, 1fr);
158
+ gap: 1.5rem;
159
+ }
160
+
161
+ .insight-card {
162
+ background: var(--light-color);
163
+ border-radius: 8px;
164
+ padding: 1.5rem;
165
+ }
166
+
167
+ .insight-card h3 {
168
+ font-size: 1rem;
169
+ font-weight: 600;
170
+ margin-bottom: 1rem;
171
+ color: var(--dark-color);
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 0.5rem;
175
+ }
176
+
177
+ .insight-card i {
178
+ color: var(--primary-color);
179
+ }
180
+
181
+ /* Data Points */
182
+ .data-point {
183
+ background: white;
184
+ border-radius: 8px;
185
+ padding: 1rem;
186
+ margin-bottom: 1rem;
187
+ border: 1px solid var(--border-color);
188
+ transition: all 0.3s ease;
189
+ }
190
+
191
+ .data-point:hover {
192
+ transform: translateY(-2px);
193
+ box-shadow: var(--shadow);
194
+ }
195
+
196
+ .data-point.anomaly {
197
+ border-left: 4px solid var(--danger-color);
198
+ }
199
+
200
+ /* Alerts */
201
+ .alert {
202
+ padding: 1rem;
203
+ border-radius: 8px;
204
+ margin-bottom: 1rem;
205
+ font-weight: 500;
206
+ }
207
+
208
+ .alert-danger {
209
+ background: #fef2f2;
210
+ color: var(--danger-color);
211
+ border: 1px solid #fee2e2;
212
+ }
213
+
214
+ .alert-success {
215
+ background: #f0fdf4;
216
+ color: var(--success-color);
217
+ border: 1px solid #dcfce7;
218
+ }
219
+
220
+ /* Responsive Design */
221
+ @media (max-width: 1024px) {
222
+ .sidebar {
223
+ width: 240px;
224
+ }
225
+ .main-content {
226
+ margin-left: 240px;
227
+ }
228
+ .dashboard-grid {
229
+ grid-template-columns: 1fr;
230
+ }
231
+ }
232
+
233
+ @media (max-width: 768px) {
234
+ .dashboard-container {
235
+ flex-direction: column;
236
+ }
237
+ .sidebar {
238
+ width: 100%;
239
+ height: auto;
240
+ position: static;
241
+ }
242
+ .main-content {
243
+ margin-left: 0;
244
+ }
245
+ .insights-grid {
246
+ grid-template-columns: 1fr;
247
+ }
248
+ }
249
+
250
+ .component-status {
251
+ background: white;
252
+ border-radius: 6px;
253
+ padding: 1rem;
254
+ margin-bottom: 1rem;
255
+ border-left: 4px solid var(--warning-color);
256
+ }
257
+
258
+ .component-status.critical {
259
+ border-left-color: var(--danger-color);
260
+ }
261
+
262
+ .status-details {
263
+ margin-top: 0.5rem;
264
+ font-size: 0.9rem;
265
+ }
266
+
267
+ .status-badge {
268
+ display: inline-block;
269
+ padding: 0.25rem 0.75rem;
270
+ border-radius: 999px;
271
+ font-size: 0.8rem;
272
+ font-weight: 500;
273
+ }
274
+
275
+ .status-badge.critical {
276
+ background: #fee2e2;
277
+ color: var(--danger-color);
278
+ }
279
+
280
+ .status-badge.warning {
281
+ background: #fef3c7;
282
+ color: var(--warning-color);
283
+ }
284
+
285
+ .maintenance-task {
286
+ background: white;
287
+ border-radius: 6px;
288
+ padding: 1rem;
289
+ margin-bottom: 1rem;
290
+ }
291
+
292
+ .task-header {
293
+ display: flex;
294
+ justify-content: space-between;
295
+ align-items: center;
296
+ margin-bottom: 0.5rem;
297
+ }
298
+
299
+ .priority-badge {
300
+ padding: 0.25rem 0.75rem;
301
+ border-radius: 999px;
302
+ font-size: 0.8rem;
303
+ font-weight: 500;
304
+ }
305
+
306
+ .priority-high .priority-badge {
307
+ background: #fee2e2;
308
+ color: var(--danger-color);
309
+ }
310
+
311
+ .priority-medium .priority-badge {
312
+ background: #fef3c7;
313
+ color: var(--warning-color);
314
+ }
315
+
316
+ .priority-low .priority-badge {
317
+ background: #dcfce7;
318
+ color: var(--success-color);
319
+ }
320
+
321
+ .task-details {
322
+ font-size: 0.9rem;
323
+ }
324
+
325
+ .task-details p {
326
+ margin: 0.25rem 0;
327
+ }
328
+
329
+ .health-overview {
330
+ text-align: center;
331
+ }
332
+
333
+ .health-score {
334
+ margin-bottom: 1.5rem;
335
+ }
336
+
337
+ .health-score .score {
338
+ font-size: 2.5rem;
339
+ font-weight: 600;
340
+ color: var(--primary-color);
341
+ }
342
+
343
+ .component-counts {
344
+ display: flex;
345
+ justify-content: space-around;
346
+ gap: 1rem;
347
+ }
348
+
349
+ .count-item {
350
+ text-align: center;
351
+ font-size: 0.9rem;
352
+ }
353
+
354
+ .count-item span {
355
+ display: block;
356
+ font-size: 1.5rem;
357
+ font-weight: 600;
358
+ margin-bottom: 0.25rem;
359
+ }
360
+
361
+ .count-item.critical span {
362
+ color: var(--danger-color);
363
+ }
364
+
365
+ .count-item.warning span {
366
+ color: var(--warning-color);
367
+ }
368
+
369
+ .count-item.good span {
370
+ color: var(--success-color);
371
+ }
372
+
373
+ /* New styles for Critical Components Analysis */
374
+ .trends-section {
375
+ margin-top: 1rem;
376
+ }
377
+
378
+ .trend-item {
379
+ margin-bottom: 0.5rem;
380
+ }
381
+
382
+ .trend-item strong {
383
+ font-weight: 600;
384
+ }
385
+
386
+ .trend-item span {
387
+ font-size: 0.9rem;
388
+ margin-left: 0.5rem;
389
+ }
390
+
391
+ .trend-item .trend-badge {
392
+ padding: 0.25rem 0.75rem;
393
+ border-radius: 999px;
394
+ font-size: 0.8rem;
395
+ font-weight: 500;
396
+ }
397
+
398
+ .trend-item .trend-badge.up {
399
+ background: #dcfce7;
400
+ color: var(--success-color);
401
+ }
402
+
403
+ .trend-item .trend-badge.down {
404
+ background: #fef2f2;
405
+ color: var(--danger-color);
406
+ }
407
+
408
+ /* New styles for Maintenance Analysis */
409
+ .maintenance-section {
410
+ margin-top: 1rem;
411
+ }
412
+
413
+ .maintenance-section h4 {
414
+ font-size: 1.25rem;
415
+ font-weight: 600;
416
+ margin-bottom: 0.5rem;
417
+ }
418
+
419
+ .maintenance-task.priority-high {
420
+ background: #fef2f2;
421
+ }
422
+
423
+ .maintenance-task.priority-medium {
424
+ background: #fef3c7;
425
+ }
426
+
427
+ .maintenance-task.priority-low {
428
+ background: #dcfce7;
429
+ }
430
+
431
+ .maintenance-task.priority-high .priority-badge {
432
+ background: #fee2e2;
433
+ color: var(--danger-color);
434
+ }
435
+
436
+ .maintenance-task.priority-medium .priority-badge {
437
+ background: #fef3c7;
438
+ color: var(--warning-color);
439
+ }
440
+
441
+ .maintenance-task.priority-low .priority-badge {
442
+ background: #dcfce7;
443
+ color: var(--success-color);
444
+ }
445
+
446
+ .preventive-measure {
447
+ margin-bottom: 0.5rem;
448
+ }
449
+
450
+ .preventive-measure h5 {
451
+ font-size: 1rem;
452
+ font-weight: 600;
453
+ margin-bottom: 0.25rem;
454
+ }
455
+
456
+ .preventive-measure p {
457
+ margin: 0.25rem 0;
458
+ }
459
+
460
+ .optimization-suggestion {
461
+ margin-bottom: 0.5rem;
462
+ }
463
+
464
+ .optimization-suggestion h5 {
465
+ font-size: 1rem;
466
+ font-weight: 600;
467
+ margin-bottom: 0.25rem;
468
+ }
469
+
470
+ .optimization-suggestion p {
471
+ margin: 0.25rem 0;
472
+ }
473
+
474
+ /* Add to your existing CSS */
475
+ .analysis-section {
476
+ display: grid;
477
+ grid-template-columns: repeat(2, 1fr);
478
+ gap: 1.5rem;
479
+ margin-bottom: 2rem;
480
+ }
481
+
482
+ .graph-card.full-width {
483
+ grid-column: 1 / -1;
484
+ }
485
+
486
+ .graph-card {
487
+ background: white;
488
+ border-radius: 12px;
489
+ padding: 1.5rem;
490
+ box-shadow: var(--shadow);
491
+ }
492
+
493
+ .graph-card h3 {
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 0.5rem;
497
+ font-size: 1.1rem;
498
+ font-weight: 600;
499
+ margin-bottom: 1rem;
500
+ color: var(--dark-color);
501
+ }
502
+
503
+ .graph-card h3 i {
504
+ color: var(--primary-color);
505
+ }
506
+ </style>
507
+ </head>
508
+ <body>
509
+ <div class="dashboard-container">
510
+ <!-- Sidebar -->
511
+ <div class="sidebar">
512
+ <div class="sidebar-header">
513
+ <h1>Maintenance Dashboard</h1>
514
+ </div>
515
+ <div class="sidebar-content">
516
+ <h3>Data Points</h3>
517
+ {% if data %}
518
+ {% for row in data %}
519
+ <div class="data-point {% if row.row_num in anomalies|map(attribute='row') %}anomaly{% endif %}">
520
+ <strong>#{{ loop.index }}</strong>
521
+ <div>Brakes: {{ row.brakes }}%</div>
522
+ <div>Filters: {{ row.filters }}%</div>
523
+ <div>Cables: {{ row.cables }}%</div>
524
+ </div>
525
+ {% endfor %}
526
+ {% endif %}
527
+ </div>
528
+ </div>
529
+
530
+ <!-- Main Content -->
531
+ <div class="main-content">
532
+ <!-- Flash Messages -->
533
+ {% with messages = get_flashed_messages(with_categories=true) %}
534
+ {% if messages %}
535
+ {% for category, message in messages %}
536
+ <div class="alert alert-{{ category }}">
537
+ <i class="fas fa-info-circle"></i> {{ message }}
538
+ </div>
539
+ {% endfor %}
540
+ {% endif %}
541
+ {% endwith %}
542
+
543
+ <!-- Upload Section -->
544
+ <div class="upload-container">
545
+ <form action="{{ url_for('upload') }}" method="post" enctype="multipart/form-data" class="upload-form">
546
+ <div class="file-input-wrapper">
547
+ <input type="file" id="file" name="file" accept=".csv,.xlsx,.xls" required class="file-input">
548
+ </div>
549
+ <button type="submit" class="upload-button">
550
+ <i class="fas fa-upload"></i> Upload & Process
551
+ </button>
552
+ </form>
553
+ <div class="upload-info">
554
+ <small>Supported formats: CSV, Excel (.xlsx, .xls) | Required columns: brakes, filters, cables</small>
555
+ </div>
556
+ </div>
557
+
558
+ {% if data %}
559
+ <!-- Graphs Section -->
560
+ <div class="dashboard-grid">
561
+ <!-- Gauge Charts -->
562
+ <div class="graph-card">
563
+ <h3><i class="fas fa-tachometer-alt"></i> Brake Reading</h3>
564
+ {{ graphs.brakes_gauge | safe }}
565
+ </div>
566
+ <div class="graph-card">
567
+ <h3><i class="fas fa-tachometer-alt"></i> Filter Reading</h3>
568
+ {{ graphs.filters_gauge | safe }}
569
+ </div>
570
+ <div class="graph-card">
571
+ <h3><i class="fas fa-tachometer-alt"></i> Cable Reading</h3>
572
+ {{ graphs.cables_gauge | safe }}
573
+ </div>
574
+
575
+ <!-- Comparison Charts -->
576
+ <div class="graph-card">
577
+ <h3><i class="fas fa-chart-bar"></i> Current vs Average Readings</h3>
578
+ {{ graphs.bar | safe }}
579
+ </div>
580
+ </div>
581
+
582
+ <!-- Additional Analysis Charts -->
583
+ <div class="analysis-section">
584
+ <div class="graph-card full-width">
585
+ <h3><i class="fas fa-chart-line"></i> Time Series Analysis with Moving Averages</h3>
586
+ {{ graphs.timeseries | safe }}
587
+ </div>
588
+
589
+ <div class="graph-card">
590
+ <h3><i class="fas fa-th"></i> Correlation Matrix</h3>
591
+ {{ graphs.heatmap | safe }}
592
+ </div>
593
+
594
+ <div class="graph-card">
595
+ <h3><i class="fas fa-box-plot"></i> Value Distributions</h3>
596
+ {{ graphs.box_plot | safe }}
597
+ </div>
598
+
599
+ <div class="graph-card full-width">
600
+ <h3><i class="fas fa-project-diagram"></i> Component Relationships</h3>
601
+ {{ graphs.scatter_matrix | safe }}
602
+ </div>
603
+ </div>
604
+
605
+ <!-- Insights Section -->
606
+ <div class="insights-container">
607
+ <div class="insights-grid">
608
+ <div class="insight-card">
609
+ <h3><i class="fas fa-exclamation-triangle"></i> Critical Components Analysis</h3>
610
+ {% if stats.detailed_analysis.insights.critical_analysis %}
611
+ {% for analysis in stats.detailed_analysis.insights.critical_analysis %}
612
+ <div class="component-status {% if analysis.severity == 'High' %}critical{% else %}warning{% endif %}">
613
+ <h4>{{ analysis.component|title }}</h4>
614
+ <div class="status-details">
615
+ <p><i class="fas fa-exclamation-circle"></i> Severity: {{ analysis.severity }}</p>
616
+ <p><i class="fas fa-chart-line"></i> Trend: {{ analysis.trend }}</p>
617
+ <p><i class="fas fa-info-circle"></i> {{ analysis.reason }}</p>
618
+ <p><i class="fas fa-impact"></i> Impact: {{ analysis.impact }}</p>
619
+ </div>
620
+ </div>
621
+ {% endfor %}
622
+ {% else %}
623
+ <p>No critical components detected</p>
624
+ {% endif %}
625
+
626
+ <!-- Component Trends -->
627
+ <div class="trends-section">
628
+ <h4>Component Trends</h4>
629
+ {% for component, trend in stats.detailed_analysis.trends.items() %}
630
+ <div class="trend-item">
631
+ <strong>{{ component|title }}:</strong>
632
+ <span class="trend-badge {{ trend.trend.lower() }}">
633
+ {{ trend.trend }}
634
+ <i class="fas fa-arrow-{{ 'up' if trend.recent_trend == 'Up' else 'down' }}"></i>
635
+ </span>
636
+ <p>Change Rate: {{ trend.rate_of_change }}% per reading</p>
637
+ <p>Peak Usage: {{ trend.peak_usage_frequency }}% of time</p>
638
+ </div>
639
+ {% endfor %}
640
+ </div>
641
+ </div>
642
+
643
+ <div class="insight-card">
644
+ <h3><i class="fas fa-tools"></i> Maintenance Analysis</h3>
645
+
646
+ <!-- Immediate Actions -->
647
+ <div class="maintenance-section">
648
+ <h4><i class="fas fa-exclamation-circle"></i> Immediate Actions</h4>
649
+ {% if stats.detailed_analysis.insights.maintenance_recommendations %}
650
+ {% for rec in stats.detailed_analysis.insights.maintenance_recommendations %}
651
+ <div class="maintenance-task priority-{{ rec.urgency.lower() }}">
652
+ <div class="task-header">
653
+ <h5>{{ rec.component|title }}</h5>
654
+ <span class="priority-badge">{{ rec.urgency }}</span>
655
+ </div>
656
+ <div class="task-details">
657
+ <p><i class="fas fa-clock"></i> {{ rec.action }}</p>
658
+ <p><i class="fas fa-info-circle"></i> {{ rec.reason }}</p>
659
+ </div>
660
+ </div>
661
+ {% endfor %}
662
+ {% else %}
663
+ <p>No immediate actions required</p>
664
+ {% endif %}
665
+ </div>
666
+
667
+ <!-- Preventive Measures -->
668
+ <div class="maintenance-section">
669
+ <h4><i class="fas fa-shield-alt"></i> Preventive Measures</h4>
670
+ {% if stats.detailed_analysis.insights.preventive_measures %}
671
+ {% for measure in stats.detailed_analysis.insights.preventive_measures %}
672
+ <div class="preventive-measure">
673
+ <h5>{{ measure.component|title }}</h5>
674
+ <p><i class="fas fa-check-circle"></i> {{ measure.measure }}</p>
675
+ <p><i class="fas fa-clock"></i> Frequency: {{ measure.frequency }}</p>
676
+ <p><i class="fas fa-info-circle"></i> {{ measure.reason }}</p>
677
+ </div>
678
+ {% endfor %}
679
+ {% endif %}
680
+ </div>
681
+
682
+ <!-- Optimization Suggestions -->
683
+ <div class="maintenance-section">
684
+ <h4><i class="fas fa-lightbulb"></i> Optimization Suggestions</h4>
685
+ {% if stats.detailed_analysis.insights.optimization_suggestions %}
686
+ {% for opt in stats.detailed_analysis.insights.optimization_suggestions %}
687
+ <div class="optimization-suggestion">
688
+ <h5>{{ opt.component|title }}</h5>
689
+ <p><i class="fas fa-star"></i> {{ opt.suggestion }}</p>
690
+ <p><i class="fas fa-chart-line"></i> Impact: {{ opt.potential_impact }}</p>
691
+ <p><i class="fas fa-check"></i> {{ opt.expected_benefit }}</p>
692
+ </div>
693
+ {% endfor %}
694
+ {% endif %}
695
+ </div>
696
+ </div>
697
+
698
+ <div class="insight-card">
699
+ <h3><i class="fas fa-chart-pie"></i> System Health Overview</h3>
700
+ <div class="health-overview">
701
+ <div class="health-score">
702
+ <h4>Overall Health</h4>
703
+ <div class="score">{{ stats.performance_metrics.overall_health }}%</div>
704
+ </div>
705
+ <div class="component-counts">
706
+ <div class="count-item critical">
707
+ <span>{{ stats.performance_metrics.critical_count }}</span>
708
+ Critical
709
+ </div>
710
+ <div class="count-item warning">
711
+ <span>{{ stats.performance_metrics.warning_count }}</span>
712
+ Warning
713
+ </div>
714
+ <div class="count-item good">
715
+ <span>{{ stats.performance_metrics.healthy_count }}</span>
716
+ Healthy
717
+ </div>
718
+ </div>
719
+ </div>
720
+ </div>
721
+ </div>
722
+ </div>
723
+
724
+ <!-- Anomalies Section -->
725
+ {% if anomalies %}
726
+ <div class="insights-container">
727
+ <h2><i class="fas fa-exclamation-circle"></i> Detected Anomalies</h2>
728
+ {% for anomaly in anomalies %}
729
+ <div class="alert alert-danger">
730
+ <strong>Row {{ anomaly.row }}:</strong>
731
+ <ul>
732
+ {% for message in anomaly.messages %}
733
+ <li>{{ message }}</li>
734
+ {% endfor %}
735
+ </ul>
736
+ </div>
737
+ {% endfor %}
738
+ </div>
739
+ {% endif %}
740
+ {% endif %}
741
+ </div>
742
+ </div>
743
+ </body>
744
+ </html>
745
+