1311 lines
46 KiB
PHP
1311 lines
46 KiB
PHP
<?php
|
||
// filepath: v:\controls\test\HMI\anom.php
|
||
include "../../session.php";
|
||
include "../../userAccess.php";
|
||
|
||
// Database connection
|
||
$servername = "192.168.0.13";
|
||
$username = "opce";
|
||
$password = "opcelasuca";
|
||
$dbname = "archive";
|
||
|
||
try {
|
||
$pdo = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
} catch (PDOException $e) {
|
||
if (isset($_POST['action'])) {
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()]);
|
||
exit;
|
||
}
|
||
die("Connection failed: " . $e->getMessage());
|
||
}
|
||
|
||
// Handle AJAX requests
|
||
if (isset($_POST['action'])) {
|
||
// Clear any previous output and set proper headers
|
||
while (ob_get_level()) {
|
||
ob_end_clean();
|
||
}
|
||
ob_start();
|
||
|
||
header('Content-Type: application/json');
|
||
|
||
try {
|
||
switch ($_POST['action']) {
|
||
case 'get_tags':
|
||
$stmt = $pdo->prepare("SELECT DISTINCT name FROM id_names WHERE name IS NOT NULL AND name != '' ORDER BY name");
|
||
$stmt->execute();
|
||
$tags = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||
echo json_encode(['success' => true, 'tags' => $tags]);
|
||
break;
|
||
|
||
case 'get_recent_data':
|
||
$selectedTagsJson = $_POST['tags'] ?? '[]';
|
||
$selectedTags = is_string($selectedTagsJson) ? json_decode($selectedTagsJson, true) : $selectedTagsJson;
|
||
$hours = (int)($_POST['hours'] ?? 24);
|
||
|
||
if (empty($selectedTags) || !is_array($selectedTags)) {
|
||
throw new Exception('No tags selected');
|
||
}
|
||
|
||
$placeholders = str_repeat('?,', count($selectedTags) - 1) . '?';
|
||
$sql = "SELECT
|
||
h.TimeStamp,
|
||
n.name as tag_name,
|
||
h.Value
|
||
FROM historicaldata h
|
||
INNER JOIN id_names n ON h.ID = n.idnumber
|
||
WHERE n.name IN ($placeholders)
|
||
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||
ORDER BY h.TimeStamp DESC, n.name ASC
|
||
LIMIT 10000";
|
||
|
||
$params = array_merge($selectedTags, [$hours]);
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
echo json_encode([
|
||
'success' => true,
|
||
'data' => $results,
|
||
'count' => count($results)
|
||
]);
|
||
break;
|
||
|
||
case 'analyze_anomalies':
|
||
$selectedTagsJson = $_POST['tags'] ?? '[]';
|
||
$selectedTags = is_string($selectedTagsJson) ? json_decode($selectedTagsJson, true) : $selectedTagsJson;
|
||
$sensitivity = (float)($_POST['sensitivity'] ?? 2.0);
|
||
$lookbackHours = (int)($_POST['lookback_hours'] ?? 168);
|
||
$analysisHours = (int)($_POST['analysis_hours'] ?? 24);
|
||
|
||
if (empty($selectedTags) || !is_array($selectedTags)) {
|
||
throw new Exception('No tags selected');
|
||
}
|
||
|
||
$anomalies = [];
|
||
|
||
foreach ($selectedTags as $tagName) {
|
||
// Get historical baseline data (excluding recent analysis period)
|
||
$baselineStmt = $pdo->prepare("
|
||
SELECT h.Value
|
||
FROM historicaldata h
|
||
INNER JOIN id_names n ON h.ID = n.idnumber
|
||
WHERE n.name = ?
|
||
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||
AND h.TimeStamp < DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||
AND h.Value IS NOT NULL
|
||
ORDER BY h.TimeStamp ASC
|
||
");
|
||
$baselineStmt->execute([$tagName, $lookbackHours, $analysisHours]);
|
||
$baselineData = $baselineStmt->fetchAll(PDO::FETCH_COLUMN);
|
||
|
||
// Get recent data for analysis
|
||
$recentStmt = $pdo->prepare("
|
||
SELECT h.Value, h.TimeStamp
|
||
FROM historicaldata h
|
||
INNER JOIN id_names n ON h.ID = n.idnumber
|
||
WHERE n.name = ?
|
||
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||
AND h.Value IS NOT NULL
|
||
ORDER BY h.TimeStamp ASC
|
||
");
|
||
$recentStmt->execute([$tagName, $analysisHours]);
|
||
$recentResults = $recentStmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
if (count($baselineData) < 10 || count($recentResults) < 5) {
|
||
continue; // Skip if insufficient data
|
||
}
|
||
|
||
// Convert baseline data to floats
|
||
$baselineValues = array_map('floatval', $baselineData);
|
||
|
||
// Calculate statistical baseline
|
||
$mean = array_sum($baselineValues) / count($baselineValues);
|
||
|
||
// Calculate standard deviation
|
||
$variance = 0;
|
||
foreach ($baselineValues as $value) {
|
||
$variance += pow($value - $mean, 2);
|
||
}
|
||
$stdDev = sqrt($variance / (count($baselineValues) - 1)); // Sample standard deviation
|
||
|
||
// Skip if standard deviation is too small (constant values)
|
||
if ($stdDev < 0.001) {
|
||
continue;
|
||
}
|
||
|
||
$upperThreshold = $mean + ($sensitivity * $stdDev);
|
||
$lowerThreshold = $mean - ($sensitivity * $stdDev);
|
||
|
||
// Check recent values for anomalies
|
||
foreach ($recentResults as $point) {
|
||
$value = (float)$point['Value'];
|
||
$timestamp = $point['TimeStamp'];
|
||
|
||
if ($value > $upperThreshold || $value < $lowerThreshold) {
|
||
$deviation = abs($value - $mean) / $stdDev;
|
||
|
||
// Determine severity based on deviation
|
||
$severity = 'medium';
|
||
if ($deviation > $sensitivity * 2) {
|
||
$severity = 'critical';
|
||
} elseif ($deviation > $sensitivity * 1.5) {
|
||
$severity = 'high';
|
||
}
|
||
|
||
$anomalies[] = [
|
||
'tag_name' => $tagName,
|
||
'timestamp' => $timestamp,
|
||
'value' => $value,
|
||
'expected_range' => [$lowerThreshold, $upperThreshold],
|
||
'baseline_mean' => $mean,
|
||
'baseline_stddev' => $stdDev,
|
||
'deviation_factor' => $deviation,
|
||
'severity' => $severity,
|
||
'message' => sprintf(
|
||
'Value %.2f is %.1f standard deviations from normal (%.2f ± %.2f)',
|
||
$value, $deviation, $mean, $stdDev
|
||
)
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort anomalies by severity and timestamp
|
||
usort($anomalies, function($a, $b) {
|
||
$severityOrder = ['low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4];
|
||
$severityDiff = ($severityOrder[$b['severity']] ?? 2) - ($severityOrder[$a['severity']] ?? 2);
|
||
if ($severityDiff !== 0) return $severityDiff;
|
||
return strtotime($b['timestamp']) - strtotime($a['timestamp']);
|
||
});
|
||
|
||
echo json_encode([
|
||
'success' => true,
|
||
'anomalies' => $anomalies,
|
||
'analysis_summary' => [
|
||
'total_anomalies' => count($anomalies),
|
||
'critical_count' => count(array_filter($anomalies, function($a) { return $a['severity'] === 'critical'; })),
|
||
'high_count' => count(array_filter($anomalies, function($a) { return $a['severity'] === 'high'; })),
|
||
'medium_count' => count(array_filter($anomalies, function($a) { return $a['severity'] === 'medium'; })),
|
||
'analysis_period' => $analysisHours . ' hours',
|
||
'baseline_period' => $lookbackHours . ' hours',
|
||
'sensitivity_level' => $sensitivity
|
||
]
|
||
]);
|
||
break;
|
||
|
||
case 'get_tag_statistics':
|
||
$tagName = $_POST['tag_name'] ?? '';
|
||
$hours = (int)($_POST['hours'] ?? 168);
|
||
|
||
if (empty($tagName)) {
|
||
throw new Exception('No tag specified');
|
||
}
|
||
|
||
$stmt = $pdo->prepare("
|
||
SELECT
|
||
COUNT(*) as data_points,
|
||
AVG(h.Value) as mean_value,
|
||
STDDEV(h.Value) as std_deviation,
|
||
MIN(h.Value) as min_value,
|
||
MAX(h.Value) as max_value,
|
||
VARIANCE(h.Value) as variance
|
||
FROM historicaldata h
|
||
INNER JOIN id_names n ON h.ID = n.idnumber
|
||
WHERE n.name = ?
|
||
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL ? HOUR)
|
||
AND h.Value IS NOT NULL
|
||
");
|
||
$stmt->execute([$tagName, $hours]);
|
||
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
echo json_encode([
|
||
'success' => true,
|
||
'statistics' => $stats,
|
||
'tag_name' => $tagName,
|
||
'analysis_period' => $hours . ' hours'
|
||
]);
|
||
break;
|
||
|
||
default:
|
||
throw new Exception('Invalid action specified');
|
||
}
|
||
|
||
} catch (Exception $e) {
|
||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||
} catch (Error $e) {
|
||
echo json_encode(['success' => false, 'error' => 'PHP Error: ' . $e->getMessage()]);
|
||
}
|
||
|
||
ob_end_flush();
|
||
exit;
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LASUCA Controls - Anomaly Detection Dashboard</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.1/dist/chartjs-adapter-moment.min.js"></script>
|
||
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #1a1a2e;
|
||
color: #fff;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.dashboard-header {
|
||
background: linear-gradient(135deg, #0f3460 0%, #e94560 100%);
|
||
padding: 20px;
|
||
text-align: center;
|
||
box-shadow: 0 4px 20px rgba(233, 69, 96, 0.3);
|
||
}
|
||
|
||
.dashboard-header h1 {
|
||
color: #fff;
|
||
font-size: 2.2rem;
|
||
margin: 0;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.dashboard-header .subtitle {
|
||
color: #f1c40f;
|
||
font-size: 1.1rem;
|
||
margin-top: 8px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.control-panel {
|
||
background: #16213e;
|
||
border-radius: 15px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||
margin-bottom: 20px;
|
||
border: 1px solid #0f3460;
|
||
}
|
||
|
||
.panel-header {
|
||
background: linear-gradient(135deg, #e94560 0%, #f39c12 100%);
|
||
color: white;
|
||
padding: 15px 25px;
|
||
font-weight: bold;
|
||
font-size: 1.1rem;
|
||
border-radius: 15px 15px 0 0;
|
||
}
|
||
|
||
.control-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.control-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.control-group {
|
||
background: #1e1e3f;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
border: 1px solid #27ae60;
|
||
}
|
||
|
||
.control-group h3 {
|
||
color: #f39c12;
|
||
margin-bottom: 12px;
|
||
font-size: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.form-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.form-row label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #ecf0f1;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.form-row input,
|
||
.form-row select {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 2px solid #34495e;
|
||
border-radius: 6px;
|
||
background: #2c3e50;
|
||
color: #ecf0f1;
|
||
font-size: 0.9rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.form-row input:focus,
|
||
.form-row select:focus {
|
||
outline: none;
|
||
border-color: #f39c12;
|
||
box-shadow: 0 0 0 3px rgba(243, 156, 18, 0.1);
|
||
}
|
||
|
||
.tag-selector {
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
border: 2px solid #34495e;
|
||
border-radius: 6px;
|
||
padding: 10px;
|
||
background: #2c3e50;
|
||
}
|
||
|
||
.tag-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid #34495e;
|
||
}
|
||
|
||
.tag-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.tag-item input[type="checkbox"] {
|
||
width: auto;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.tag-item label {
|
||
margin: 0;
|
||
cursor: pointer;
|
||
flex: 1;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
background: linear-gradient(135deg, #e94560 0%, #f39c12 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
min-width: 120px;
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.4);
|
||
}
|
||
|
||
button:disabled {
|
||
background: #34495e;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
button.analyze-btn {
|
||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||
}
|
||
|
||
button.clear-btn {
|
||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||
}
|
||
|
||
.dashboard-grid {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.anomaly-alerts {
|
||
background: #16213e;
|
||
border-radius: 15px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||
border: 1px solid #e74c3c;
|
||
}
|
||
|
||
.alerts-header {
|
||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||
color: white;
|
||
padding: 15px 20px;
|
||
font-weight: bold;
|
||
border-radius: 15px 15px 0 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.alert-summary {
|
||
background: #16213e;
|
||
border-radius: 15px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||
border: 1px solid #f39c12;
|
||
}
|
||
|
||
.summary-header {
|
||
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
|
||
color: white;
|
||
padding: 15px 20px;
|
||
font-weight: bold;
|
||
border-radius: 15px 15px 0 0;
|
||
}
|
||
|
||
.alerts-container {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
padding: 15px;
|
||
}
|
||
|
||
.alert-item {
|
||
background: #1e1e3f;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
border-left: 4px solid transparent;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.alert-item:hover {
|
||
transform: translateX(5px);
|
||
}
|
||
|
||
.alert-item.critical {
|
||
border-left-color: #e74c3c;
|
||
background: rgba(231, 76, 60, 0.1);
|
||
}
|
||
|
||
.alert-item.high {
|
||
border-left-color: #f39c12;
|
||
background: rgba(243, 156, 18, 0.1);
|
||
}
|
||
|
||
.alert-item.medium {
|
||
border-left-color: #f1c40f;
|
||
background: rgba(241, 196, 15, 0.1);
|
||
}
|
||
|
||
.alert-tag {
|
||
font-weight: bold;
|
||
color: #3498db;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.alert-message {
|
||
color: #ecf0f1;
|
||
font-size: 0.85rem;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.alert-timestamp {
|
||
color: #95a5a6;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.alert-severity {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 0.7rem;
|
||
font-weight: bold;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.severity-critical {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.severity-high {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.severity-medium {
|
||
background: #f1c40f;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.summary-stats {
|
||
padding: 20px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #34495e;
|
||
}
|
||
|
||
.stat-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #bdc3c7;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: bold;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.stat-critical { color: #e74c3c; }
|
||
.stat-high { color: #f39c12; }
|
||
.stat-medium { color: #f1c40f; }
|
||
.stat-normal { color: #27ae60; }
|
||
|
||
.charts-container {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 20px;
|
||
}
|
||
|
||
.chart-container {
|
||
background: #16213e;
|
||
border-radius: 15px;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||
border: 1px solid #3498db;
|
||
}
|
||
|
||
.chart-header {
|
||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||
color: white;
|
||
padding: 15px 20px;
|
||
font-weight: bold;
|
||
border-radius: 15px 15px 0 0;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
position: relative;
|
||
height: 400px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.loading-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(26, 26, 46, 0.95);
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 9999;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 60px;
|
||
height: 60px;
|
||
border: 4px solid #34495e;
|
||
border-top: 4px solid #e94560;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.status-message {
|
||
padding: 12px 20px;
|
||
margin-bottom: 20px;
|
||
border-radius: 8px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-message.success {
|
||
background: rgba(39, 174, 96, 0.2);
|
||
border: 1px solid #27ae60;
|
||
color: #2ecc71;
|
||
}
|
||
|
||
.status-message.error {
|
||
background: rgba(231, 76, 60, 0.2);
|
||
border: 1px solid #e74c3c;
|
||
color: #e74c3c;
|
||
}
|
||
|
||
.status-message.info {
|
||
background: rgba(52, 152, 219, 0.2);
|
||
border: 1px solid #3498db;
|
||
color: #3498db;
|
||
}
|
||
|
||
.auto-refresh {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.auto-refresh input[type="checkbox"] {
|
||
width: auto;
|
||
}
|
||
|
||
.debug-info {
|
||
background: #34495e;
|
||
color: #ecf0f1;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.8rem;
|
||
border: 2px solid #3498db;
|
||
display: none;
|
||
}
|
||
|
||
.debug-info h4 {
|
||
color: #3498db;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 1200px) {
|
||
.dashboard-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.control-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
height: 300px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<!-- Loading Overlay -->
|
||
<div class="loading-overlay" id="loadingOverlay">
|
||
<div class="loading-spinner"></div>
|
||
</div>
|
||
|
||
<!-- Header -->
|
||
<div class="dashboard-header">
|
||
<h1>🚨 LASUCA Controls - Anomaly Detection Dashboard</h1>
|
||
<p class="subtitle">Real-Time Equipment Monitoring & Predictive Analytics</p>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<!-- Control Panel -->
|
||
<div class="control-panel">
|
||
<div class="panel-header">
|
||
🔧 Analysis Configuration
|
||
</div>
|
||
<div class="control-content">
|
||
<div class="control-grid">
|
||
<!-- Tag Selection -->
|
||
<div class="control-group">
|
||
<h3>🏷️ Monitor Tags</h3>
|
||
<div class="form-row">
|
||
<label>Select Equipment Tags:</label>
|
||
<div class="tag-selector" id="tagSelector">
|
||
<div style="text-align: center; padding: 20px; color: #7f8c8d;">
|
||
Loading available tags...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<button onclick="selectAllTags()">Select All</button>
|
||
<button onclick="clearAllTags()" class="clear-btn">Clear All</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Analysis Settings -->
|
||
<div class="control-group">
|
||
<h3>⚙️ Detection Settings</h3>
|
||
<div class="form-row">
|
||
<label for="sensitivity">Sensitivity Level:</label>
|
||
<select id="sensitivity">
|
||
<option value="1.5">High Sensitivity (1.5σ)</option>
|
||
<option value="2.0" selected>Normal Sensitivity (2.0σ)</option>
|
||
<option value="2.5">Low Sensitivity (2.5σ)</option>
|
||
<option value="3.0">Very Low Sensitivity (3.0σ)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="lookbackHours">Baseline Period:</label>
|
||
<select id="lookbackHours">
|
||
<option value="24">Last 24 Hours</option>
|
||
<option value="72">Last 3 Days</option>
|
||
<option value="168" selected>Last Week</option>
|
||
<option value="336">Last 2 Weeks</option>
|
||
<option value="720">Last Month</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="analysisHours">Analysis Window:</label>
|
||
<select id="analysisHours">
|
||
<option value="1">Last Hour</option>
|
||
<option value="6">Last 6 Hours</option>
|
||
<option value="24" selected>Last 24 Hours</option>
|
||
<option value="48">Last 48 Hours</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auto Refresh -->
|
||
<div class="control-group">
|
||
<h3>🔄 Real-Time Monitoring</h3>
|
||
<div class="auto-refresh">
|
||
<input type="checkbox" id="autoRefresh" onchange="toggleAutoRefresh()">
|
||
<label for="autoRefresh">Auto-refresh every 30 seconds</label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>Last Analysis:</label>
|
||
<div id="lastUpdate" style="color: #f39c12; font-weight: bold;">Never</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="action-buttons">
|
||
<button onclick="analyzeAnomalies()" class="analyze-btn" id="analyzeBtn">
|
||
🔍 Detect Anomalies
|
||
</button>
|
||
<button onclick="clearResults()" class="clear-btn">
|
||
🗑️ Clear Results
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Messages -->
|
||
<div id="statusMessages"></div>
|
||
|
||
<!-- Debug Info -->
|
||
<div id="debugInfo" class="debug-info"></div>
|
||
|
||
<!-- Dashboard Grid -->
|
||
<div class="dashboard-grid" id="dashboardGrid" style="display: none;">
|
||
<!-- Anomaly Alerts -->
|
||
<div class="anomaly-alerts">
|
||
<div class="alerts-header">
|
||
<span>🚨 Active Anomalies</span>
|
||
<span id="alertCount" class="alert-count">0</span>
|
||
</div>
|
||
<div class="alerts-container" id="alertsContainer">
|
||
<div style="text-align: center; padding: 40px; color: #7f8c8d;">
|
||
No anomalies detected
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Summary Statistics -->
|
||
<div class="alert-summary">
|
||
<div class="summary-header">
|
||
📊 Analysis Summary
|
||
</div>
|
||
<div class="summary-stats" id="summaryStats">
|
||
<div class="stat-row">
|
||
<span class="stat-label">Total Anomalies:</span>
|
||
<span class="stat-value stat-normal" id="totalAnomalies">0</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Critical:</span>
|
||
<span class="stat-value stat-critical" id="criticalCount">0</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">High Priority:</span>
|
||
<span class="stat-value stat-high" id="highCount">0</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Medium Priority:</span>
|
||
<span class="stat-value stat-medium" id="mediumCount">0</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Analysis Period:</span>
|
||
<span class="stat-value" id="analysisPeriod">-</span>
|
||
</div>
|
||
<div class="stat-row">
|
||
<span class="stat-label">Baseline Period:</span>
|
||
<span class="stat-value" id="baselinePeriod">-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts Container -->
|
||
<div class="charts-container" id="chartsContainer" style="display: none;">
|
||
<div class="chart-container">
|
||
<div class="chart-header">
|
||
📈 Real-Time Data with Anomaly Detection
|
||
</div>
|
||
<div class="chart-wrapper">
|
||
<canvas id="anomalyChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Global variables
|
||
let availableTags = [];
|
||
let currentAnomalies = [];
|
||
let autoRefreshInterval = null;
|
||
let anomalyChart = null;
|
||
|
||
// Initialize the application
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadAvailableTags();
|
||
});
|
||
|
||
// Load available tags
|
||
async function loadAvailableTags() {
|
||
try {
|
||
showLoading(true);
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'get_tags');
|
||
|
||
const response = await fetch(window.location.href, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
console.log('Raw response:', responseText);
|
||
|
||
let data;
|
||
try {
|
||
data = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
console.error('JSON parse error:', parseError);
|
||
showDebugInfo('Response Parse Error', responseText);
|
||
throw new Error('Invalid JSON response from server');
|
||
}
|
||
|
||
if (data.success) {
|
||
availableTags = data.tags;
|
||
renderTagSelector();
|
||
showStatus(`${availableTags.length} equipment tags loaded successfully`, 'success');
|
||
} else {
|
||
throw new Error(data.error || 'Failed to load tags');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading tags:', error);
|
||
showStatus('Failed to load equipment tags: ' + error.message, 'error');
|
||
} finally {
|
||
showLoading(false);
|
||
}
|
||
}
|
||
|
||
// Render tag selector
|
||
function renderTagSelector() {
|
||
const selector = document.getElementById('tagSelector');
|
||
|
||
if (availableTags.length === 0) {
|
||
selector.innerHTML = '<div style="text-align: center; padding: 20px; color: #e74c3c;">No equipment tags found</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
availableTags.forEach((tag, index) => {
|
||
const safeId = `tag_${index}`;
|
||
html += `
|
||
<div class="tag-item">
|
||
<input type="checkbox" id="${safeId}" value="${tag}">
|
||
<label for="${safeId}">${tag}</label>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
selector.innerHTML = html;
|
||
}
|
||
|
||
// Select/clear all tags
|
||
function selectAllTags() {
|
||
const checkboxes = document.querySelectorAll('#tagSelector input[type="checkbox"]');
|
||
checkboxes.forEach(cb => cb.checked = true);
|
||
}
|
||
|
||
function clearAllTags() {
|
||
const checkboxes = document.querySelectorAll('#tagSelector input[type="checkbox"]');
|
||
checkboxes.forEach(cb => cb.checked = false);
|
||
}
|
||
|
||
// Get selected tags
|
||
function getSelectedTags() {
|
||
const checkboxes = document.querySelectorAll('#tagSelector input[type="checkbox"]:checked');
|
||
return Array.from(checkboxes).map(cb => cb.value);
|
||
}
|
||
|
||
// Analyze anomalies
|
||
async function analyzeAnomalies() {
|
||
try {
|
||
const selectedTags = getSelectedTags();
|
||
if (selectedTags.length === 0) {
|
||
showStatus('Please select at least one equipment tag to monitor', 'error');
|
||
return;
|
||
}
|
||
|
||
showLoading(true);
|
||
document.getElementById('analyzeBtn').disabled = true;
|
||
|
||
const formData = new FormData();
|
||
formData.append('action', 'analyze_anomalies');
|
||
formData.append('tags', JSON.stringify(selectedTags));
|
||
formData.append('sensitivity', document.getElementById('sensitivity').value);
|
||
formData.append('lookback_hours', document.getElementById('lookbackHours').value);
|
||
formData.append('analysis_hours', document.getElementById('analysisHours').value);
|
||
|
||
console.log('Sending analysis request with tags:', selectedTags);
|
||
|
||
const response = await fetch(window.location.href, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const responseText = await response.text();
|
||
console.log('Analysis response:', responseText);
|
||
|
||
let result;
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
console.error('JSON parse error:', parseError);
|
||
showDebugInfo('Analysis Response Parse Error', responseText);
|
||
throw new Error('Invalid JSON response from server');
|
||
}
|
||
|
||
if (result.success) {
|
||
currentAnomalies = result.anomalies;
|
||
displayAnomalies(result.anomalies, result.analysis_summary);
|
||
await loadChartData(selectedTags);
|
||
showDashboard();
|
||
updateLastUpdateTime();
|
||
|
||
const summary = result.analysis_summary;
|
||
let message = `Analysis complete! Found ${summary.total_anomalies} anomalies`;
|
||
if (summary.critical_count > 0) {
|
||
message += ` (${summary.critical_count} CRITICAL)`;
|
||
}
|
||
showStatus(message, summary.critical_count > 0 ? 'error' : 'success');
|
||
} else {
|
||
throw new Error(result.error || 'Failed to analyze anomalies');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error analyzing anomalies:', error);
|
||
showStatus('Failed to analyze anomalies: ' + error.message, 'error');
|
||
} finally {
|
||
showLoading(false);
|
||
document.getElementById('analyzeBtn').disabled = false;
|
||
}
|
||
}
|
||
|
||
// Display anomalies
|
||
function displayAnomalies(anomalies, summary) {
|
||
const container = document.getElementById('alertsContainer');
|
||
const alertCount = document.getElementById('alertCount');
|
||
|
||
alertCount.textContent = anomalies.length;
|
||
|
||
if (anomalies.length === 0) {
|
||
container.innerHTML = `
|
||
<div style="text-align: center; padding: 40px; color: #27ae60;">
|
||
<h3>✅ All Systems Normal</h3>
|
||
<p>No anomalies detected in the analyzed period</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
let html = '';
|
||
anomalies.forEach(anomaly => {
|
||
const severityClass = `severity-${anomaly.severity}`;
|
||
html += `
|
||
<div class="alert-item ${anomaly.severity}">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<div class="alert-tag">${anomaly.tag_name}</div>
|
||
<span class="alert-severity ${severityClass}">${anomaly.severity}</span>
|
||
</div>
|
||
<div class="alert-message">${anomaly.message}</div>
|
||
<div class="alert-timestamp">📅 ${new Date(anomaly.timestamp).toLocaleString()}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// Update summary statistics
|
||
document.getElementById('totalAnomalies').textContent = summary.total_anomalies;
|
||
document.getElementById('criticalCount').textContent = summary.critical_count;
|
||
document.getElementById('highCount').textContent = summary.high_count;
|
||
document.getElementById('mediumCount').textContent = summary.medium_count;
|
||
document.getElementById('analysisPeriod').textContent = summary.analysis_period;
|
||
document.getElementById('baselinePeriod').textContent = summary.baseline_period;
|
||
}
|
||
|
||
// Load chart data
|
||
async function loadChartData(selectedTags) {
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('action', 'get_recent_data');
|
||
formData.append('tags', JSON.stringify(selectedTags));
|
||
formData.append('hours', document.getElementById('analysisHours').value);
|
||
|
||
const response = await fetch(window.location.href, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
renderAnomalyChart(result.data, selectedTags);
|
||
} else {
|
||
console.error('Failed to load chart data:', result.error);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading chart data:', error);
|
||
}
|
||
}
|
||
|
||
// Render anomaly chart
|
||
function renderAnomalyChart(data, selectedTags) {
|
||
const ctx = document.getElementById('anomalyChart').getContext('2d');
|
||
|
||
if (anomalyChart) {
|
||
anomalyChart.destroy();
|
||
}
|
||
|
||
// Organize data by tag
|
||
const tagData = {};
|
||
selectedTags.forEach(tag => {
|
||
tagData[tag] = [];
|
||
});
|
||
|
||
data.forEach(point => {
|
||
if (tagData[point.tag_name]) {
|
||
tagData[point.tag_name].push({
|
||
x: point.TimeStamp,
|
||
y: parseFloat(point.Value)
|
||
});
|
||
}
|
||
});
|
||
|
||
// Create datasets
|
||
const datasets = [];
|
||
const colors = ['#3498db', '#e74c3c', '#27ae60', '#f39c12', '#9b59b6', '#1abc9c'];
|
||
|
||
selectedTags.forEach((tag, index) => {
|
||
const color = colors[index % colors.length];
|
||
datasets.push({
|
||
label: tag,
|
||
data: tagData[tag] || [],
|
||
borderColor: color,
|
||
backgroundColor: color + '20',
|
||
borderWidth: 2,
|
||
fill: false,
|
||
tension: 0.1,
|
||
pointRadius: 2,
|
||
pointHoverRadius: 5
|
||
});
|
||
});
|
||
|
||
// Add anomaly points
|
||
if (currentAnomalies.length > 0) {
|
||
const anomalyPoints = currentAnomalies.map(anomaly => ({
|
||
x: anomaly.timestamp,
|
||
y: anomaly.value
|
||
}));
|
||
|
||
datasets.push({
|
||
label: 'Anomalies',
|
||
data: anomalyPoints,
|
||
backgroundColor: '#e74c3c',
|
||
borderColor: '#e74c3c',
|
||
pointRadius: 6,
|
||
pointHoverRadius: 8,
|
||
showLine: false,
|
||
pointStyle: 'triangle'
|
||
});
|
||
}
|
||
|
||
anomalyChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: { datasets: datasets },
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: {
|
||
x: {
|
||
type: 'time',
|
||
time: {
|
||
displayFormats: {
|
||
hour: 'MMM DD HH:mm',
|
||
day: 'MMM DD'
|
||
}
|
||
},
|
||
title: {
|
||
display: true,
|
||
text: 'Time',
|
||
color: '#ecf0f1'
|
||
},
|
||
ticks: { color: '#bdc3c7' },
|
||
grid: { color: '#34495e' }
|
||
},
|
||
y: {
|
||
title: {
|
||
display: true,
|
||
text: 'Value',
|
||
color: '#ecf0f1'
|
||
},
|
||
ticks: { color: '#bdc3c7' },
|
||
grid: { color: '#34495e' }
|
||
}
|
||
},
|
||
plugins: {
|
||
legend: {
|
||
labels: { color: '#ecf0f1' }
|
||
},
|
||
tooltip: {
|
||
mode: 'nearest',
|
||
intersect: false,
|
||
backgroundColor: '#2c3e50',
|
||
titleColor: '#ecf0f1',
|
||
bodyColor: '#ecf0f1',
|
||
borderColor: '#34495e',
|
||
borderWidth: 1
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Auto-refresh functionality
|
||
function toggleAutoRefresh() {
|
||
const autoRefresh = document.getElementById('autoRefresh');
|
||
|
||
if (autoRefresh.checked) {
|
||
autoRefreshInterval = setInterval(() => {
|
||
if (getSelectedTags().length > 0) {
|
||
analyzeAnomalies();
|
||
}
|
||
}, 30000); // 30 seconds
|
||
} else {
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
autoRefreshInterval = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update last update time
|
||
function updateLastUpdateTime() {
|
||
document.getElementById('lastUpdate').textContent = new Date().toLocaleString();
|
||
}
|
||
|
||
// Show dashboard
|
||
function showDashboard() {
|
||
document.getElementById('dashboardGrid').style.display = 'grid';
|
||
document.getElementById('chartsContainer').style.display = 'block';
|
||
}
|
||
|
||
// Clear results
|
||
function clearResults() {
|
||
document.getElementById('dashboardGrid').style.display = 'none';
|
||
document.getElementById('chartsContainer').style.display = 'none';
|
||
if (anomalyChart) {
|
||
anomalyChart.destroy();
|
||
anomalyChart = null;
|
||
}
|
||
currentAnomalies = [];
|
||
clearStatus();
|
||
hideDebugInfo();
|
||
document.getElementById('lastUpdate').textContent = 'Never';
|
||
|
||
// Stop auto-refresh
|
||
document.getElementById('autoRefresh').checked = false;
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
autoRefreshInterval = null;
|
||
}
|
||
}
|
||
|
||
// Debug information functions
|
||
function showDebugInfo(title, content) {
|
||
const debugDiv = document.getElementById('debugInfo');
|
||
debugDiv.innerHTML = `
|
||
<h4>${title}</h4>
|
||
<pre>${content}</pre>
|
||
`;
|
||
debugDiv.style.display = 'block';
|
||
}
|
||
|
||
function hideDebugInfo() {
|
||
document.getElementById('debugInfo').style.display = 'none';
|
||
}
|
||
|
||
// Utility functions
|
||
function showLoading(show) {
|
||
document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
|
||
}
|
||
|
||
function showStatus(message, type) {
|
||
const container = document.getElementById('statusMessages');
|
||
const div = document.createElement('div');
|
||
div.className = `status-message ${type}`;
|
||
div.textContent = message;
|
||
|
||
container.innerHTML = '';
|
||
container.appendChild(div);
|
||
|
||
if (type === 'success') {
|
||
setTimeout(() => {
|
||
if (div.parentNode === container) {
|
||
container.removeChild(div);
|
||
}
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
function clearStatus() {
|
||
document.getElementById('statusMessages').innerHTML = '';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|