format('Y-m-d H:i:s.u')))->setTimezone($tz); } if (!is_string($value) || $value === '') { return null; } $patterns = ['Y-m-d H:i:s.u', 'Y-m-d H:i:s']; foreach ($patterns as $pattern) { $dt = DateTimeImmutable::createFromFormat($pattern, $value, $tz); if ($dt instanceof DateTimeImmutable) { return $dt; } } try { return new DateTimeImmutable($value, $tz); } catch (Exception $exception) { return null; } } try { $pdo = new PDO( "sqlsrv:Server=$servername;Database=$dbname", $username, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); } catch (PDOException $exception) { if (isset($_POST['action'])) { header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'Database connection failed: ' . $exception->getMessage(), ]); exit; } die('Connection failed: ' . $exception->getMessage()); } if (isset($_POST['action'])) { ob_clean(); header('Content-Type: application/json'); try { switch ($_POST['action']) { case 'get_tag_health': $windowHours = sanitizeWindowHours( $_POST['window_hours'] ?? null, $windowOptions, $defaultWindow ); $limit = sanitizeLimit( $_POST['limit'] ?? null, $limitOptions, $defaultLimit ); $flatlineTolerance = sanitizeTolerance( $_POST['flatline_tolerance'] ?? null, $defaultTolerance ); $search = sanitizeSearch($_POST['search'] ?? ''); $params = [-$windowHours]; $sql = " WITH recent AS ( SELECT n.name, h.TimeStamp, CAST(h.Value AS float) AS value, LAG(CAST(h.Value AS float)) OVER (PARTITION BY n.name ORDER BY h.TimeStamp) AS prev_value FROM dbo.archive AS h INNER JOIN dbo.id_names AS n ON h.ID = n.idnumber WHERE h.TimeStamp >= DATEADD(HOUR, CAST(? AS INT), GETDATE()) AND n.name IS NOT NULL AND n.name <> '' "; if ($search !== '') { $sql .= " AND n.name LIKE ?\n"; $params[] = '%' . $search . '%'; } $sql .= ") , aggregated AS ( SELECT name, COUNT(*) AS sample_count, SUM(CASE WHEN value IS NULL THEN 1 ELSE 0 END) AS null_count, SUM( CASE WHEN prev_value IS NOT NULL AND value IS NOT NULL AND ABS(prev_value - value) <= CAST(? AS float) THEN 1 ELSE 0 END ) AS flatline_count, MIN(TimeStamp) AS first_timestamp, MAX(TimeStamp) AS last_timestamp, MIN(value) AS min_value, MAX(value) AS max_value, AVG(value) AS avg_value FROM recent GROUP BY name ) SELECT TOP {$limit} name, sample_count, null_count, flatline_count, first_timestamp, last_timestamp, min_value, max_value, avg_value FROM aggregated ORDER BY name ASC; "; $params[] = $flatlineTolerance; $stmt = $pdo->prepare($sql); $stmt->execute($params); $rows = $stmt->fetchAll(); $now = new DateTimeImmutable('now', $appTimeZone); $windowMinutes = $windowHours * 60; $staleThreshold = max(30, (int) round($windowMinutes * 0.2)); $tags = []; $healthyCount = 0; $warningCount = 0; $offlineCount = 0; foreach ($rows as $row) { $name = (string) $row['name']; $sampleCount = (int) ($row['sample_count'] ?? 0); $nullCount = (int) ($row['null_count'] ?? 0); $flatlineCount = (int) ($row['flatline_count'] ?? 0); $lastTimestamp = createDateTimeFromSql($row['last_timestamp'] ?? null, $appTimeZone); $firstTimestamp = createDateTimeFromSql($row['first_timestamp'] ?? null, $appTimeZone); $stalenessMinutes = null; if ($lastTimestamp instanceof DateTimeImmutable) { $stalenessMinutes = max( 0, (int) floor(($now->getTimestamp() - $lastTimestamp->getTimestamp()) / 60) ); } $coverageMinutes = null; if ( $firstTimestamp instanceof DateTimeImmutable && $lastTimestamp instanceof DateTimeImmutable ) { $coverageMinutes = max( 0, (int) floor(($lastTimestamp->getTimestamp() - $firstTimestamp->getTimestamp()) / 60) ); } $uptimePercent = $sampleCount > 0 ? max(0, (1 - ($nullCount / $sampleCount)) * 100) : 0.0; $flatlinePercent = $sampleCount > 1 ? max(0, ($flatlineCount / max(1, $sampleCount - 1)) * 100) : 0.0; $minValue = $row['min_value'] !== null ? (float) $row['min_value'] : null; $maxValue = $row['max_value'] !== null ? (float) $row['max_value'] : null; $avgValue = $row['avg_value'] !== null ? (float) $row['avg_value'] : null; $range = null; if ($minValue !== null && $maxValue !== null) { $range = $maxValue - $minValue; } $status = 'healthy'; $issues = []; if ($sampleCount === 0 || $lastTimestamp === null) { $status = 'offline'; $issues[] = 'No samples in the selected window.'; } elseif ($stalenessMinutes !== null && $stalenessMinutes > $windowMinutes) { $status = 'offline'; $issues[] = 'No updates within the entire analysis window.'; } else { if ($stalenessMinutes !== null && $stalenessMinutes > $staleThreshold) { $issues[] = 'Data appears stale.'; } if ($uptimePercent < 90) { $issues[] = 'High null rate detected.'; } if ($flatlinePercent > 80) { $issues[] = 'Likely flatlined signal.'; } if ($range !== null && abs($range) <= $flatlineTolerance) { $issues[] = 'Minimal variance across window.'; } if (!empty($issues)) { $status = 'warning'; } } if ($status === 'healthy') { $healthyCount++; } elseif ($status === 'warning') { $warningCount++; } else { $offlineCount++; } $averageSpacing = null; if ($coverageMinutes !== null && $sampleCount > 1) { $averageSpacing = $coverageMinutes / max(1, $sampleCount - 1); } $tags[] = [ 'name' => $name, 'status' => $status, 'issues' => $issues, 'sampleCount' => $sampleCount, 'nullCount' => $nullCount, 'flatlineCount' => $flatlineCount, 'uptimePercent' => round($uptimePercent, 1), 'flatlinePercent' => round($flatlinePercent, 1), 'minValue' => $minValue, 'maxValue' => $maxValue, 'avgValue' => $avgValue, 'range' => $range !== null ? round($range, 6) : null, 'lastTimestamp' => $lastTimestamp ? $lastTimestamp->format('Y-m-d H:i:s') : null, 'minutesSinceUpdate' => $stalenessMinutes, 'firstTimestamp' => $firstTimestamp ? $firstTimestamp->format('Y-m-d H:i:s') : null, 'windowCoverageMinutes' => $coverageMinutes, 'averageSpacingMinutes' => $averageSpacing !== null ? round($averageSpacing, 1) : null, ]; } echo json_encode([ 'success' => true, 'windowHours' => $windowHours, 'limit' => $limit, 'flatlineTolerance' => $flatlineTolerance, 'generatedAt' => $now->format('Y-m-d H:i:s'), 'totals' => [ 'total' => count($tags), 'healthy' => $healthyCount, 'warning' => $warningCount, 'offline' => $offlineCount, ], 'tags' => $tags, ]); break; default: throw new RuntimeException('Unsupported action.'); } } catch (Throwable $exception) { echo json_encode([ 'success' => false, 'error' => $exception->getMessage(), ]); } exit; } require __DIR__ . '/../includes/layout/header.php'; ?>
Surface historian tags with stale updates, high null rates, or flatlined readings over the selected time window.
Signals with a change smaller than this value between samples are treated as flatlined.
Sorted by tag name. Use the filters to focus on problem tags.
| Tag | Status | Last update | Staleness | Samples | Uptime % | Flatline % | Range (min → max) | Issues |
|---|---|---|---|---|---|---|---|---|
| Run the audit to populate results. | ||||||||