PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); } catch (PDOException $e) { die("Connection failed: " . $e->getMessage()); } $isAutocomplete = isset($_GET['action']) && $_GET['action'] === 'autocomplete'; if ($isAutocomplete) { $query = isset($_GET['query']) ? trim($_GET['query']) : ''; $suggestions = []; if ($query !== '') { try { $stmt = $pdo->prepare(" SELECT TOP 20 name FROM dbo.id_names WHERE name LIKE :query ORDER BY name "); $stmt->execute([':query' => '%' . $query . '%']); $rows = $stmt->fetchAll(); foreach ($rows as $row) { if (!empty($row['name'])) { $suggestions[] = $row['name']; } } } catch (PDOException $e) { error_log('Autocomplete error: ' . $e->getMessage()); } } header('Content-Type: application/json'); echo json_encode($suggestions); exit; } $allTags = []; try { $tagStmt = $pdo->prepare(" SELECT DISTINCT name FROM dbo.id_names WHERE name <> '' ORDER BY name "); $tagStmt->execute(); $allTags = $tagStmt->fetchAll(); } catch (PDOException $e) { error_log('Tag preload error: ' . $e->getMessage()); } $searchType = isset($_GET['search_type']) ? $_GET['search_type'] : 'single_name'; $searchName = isset($_GET['name']) ? trim($_GET['name']) : ''; $selectedTags = []; for ($i = 1; $i <= 5; $i++) { $param = "tag{$i}"; if (!empty($_GET[$param])) { $selectedTags[] = trim($_GET[$param]); } } $legacyMultiple = isset($_GET['multiple_names']) ? trim($_GET['multiple_names']) : ''; if ($legacyMultiple !== '' && empty($selectedTags)) { $selectedTags = array_filter(array_map('trim', explode(',', $legacyMultiple))); } $startDateInput = isset($_GET['start_date']) ? $_GET['start_date'] : ''; $endDateInput = isset($_GET['end_date']) ? $_GET['end_date'] : ''; $normalizeDateTime = function ($value) { if ($value === '') { return ''; } $patterns = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i']; foreach ($patterns as $pattern) { $dt = DateTime::createFromFormat($pattern, $value); if ($dt instanceof DateTime) { return $dt->format('Y-m-d H:i:s'); } } return $value; }; $startDate = $normalizeDateTime($startDateInput); $endDate = $normalizeDateTime($endDateInput); $limit = isset($_GET['limit']) ? (int) $_GET['limit'] : 100; if ($limit <= 0) { $limit = 100; } $showTrend = isset($_GET['show_trend']); $timeInterval = isset($_GET['time_interval']) ? (int) $_GET['time_interval'] : 30; if ($timeInterval < 1) { $timeInterval = 1; } $results = []; $searchTitle = ''; $totalCount = 0; $tagNames = []; $organizedResults = []; $canSearch = false; switch ($searchType) { case 'single_name': $canSearch = $searchName !== ''; break; case 'multiple_names': $canSearch = !empty($selectedTags); break; } if ($canSearch) { try { $sql = " SELECT a.ID, a.Value, a.TimeStamp, COALESCE(n.name, CONCAT('ID_', a.ID)) AS name FROM dbo.archive a LEFT JOIN dbo.id_names n ON a.ID = n.idnumber WHERE 1 = 1 AND a.TimeStamp <> :invalid_timestamp "; $countSql = " SELECT COUNT(*) FROM dbo.archive a LEFT JOIN dbo.id_names n ON a.ID = n.idnumber WHERE 1 = 1 AND a.TimeStamp <> :invalid_timestamp "; $params = []; $params[':invalid_timestamp'] = '1899-12-31 18:00:00'; switch ($searchType) { case 'single_name': $sql .= " AND n.name = :name"; $countSql .= " AND n.name = :name"; $params[':name'] = $searchName; $searchTitle = $searchName; break; case 'multiple_names': if (!empty($selectedTags)) { $placeholders = []; foreach ($selectedTags as $index => $tag) { $placeholder = ':name' . $index; $placeholders[] = $placeholder; $params[$placeholder] = $tag; } $inClause = implode(',', $placeholders); $sql .= " AND n.name IN ($inClause)"; $countSql .= " AND n.name IN ($inClause)"; $preview = array_slice($selectedTags, 0, 3); $searchTitle = 'Multiple tags (' . count($selectedTags) . '): ' . implode(', ', $preview); if (count($selectedTags) > 3) { $searchTitle .= '…'; } } break; } if ($startDate !== '') { $sql .= " AND a.TimeStamp >= :start_date"; $countSql .= " AND a.TimeStamp >= :start_date"; $params[':start_date'] = $startDate; } if ($endDate !== '') { $sql .= " AND a.TimeStamp <= :end_date"; $countSql .= " AND a.TimeStamp <= :end_date"; $params[':end_date'] = $endDate; } if ($timeInterval > 1) { if ($timeInterval >= 60) { $minutes = (int) ($timeInterval / 60); $sql .= " AND DATEPART(MINUTE, a.TimeStamp) % :minute_interval = 0 AND DATEPART(SECOND, a.TimeStamp) = 0 "; $countSql .= " AND DATEPART(MINUTE, a.TimeStamp) % :minute_interval = 0 AND DATEPART(SECOND, a.TimeStamp) = 0 "; $params[':minute_interval'] = $minutes; } else { $sql .= " AND DATEPART(SECOND, a.TimeStamp) % :time_interval = 0"; $countSql .= " AND DATEPART(SECOND, a.TimeStamp) % :time_interval = 0"; $params[':time_interval'] = $timeInterval; } } $countStmt = $pdo->prepare($countSql); foreach ($params as $key => $value) { if ($key === ':limit') { continue; } $countStmt->bindValue($key, $value); } $countStmt->execute(); $totalCount = (int) $countStmt->fetchColumn(); if ($showTrend) { $sql .= " ORDER BY a.TimeStamp ASC OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY "; } else { $sql .= " ORDER BY a.TimeStamp DESC OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY "; } $params[':limit'] = $limit; $stmt = $pdo->prepare($sql); foreach ($params as $key => $value) { if ($key === ':limit') { $stmt->bindValue($key, $value, PDO::PARAM_INT); } else { $stmt->bindValue($key, $value); } } $queryStartTime = microtime(true); $stmt->execute(); $results = $stmt->fetchAll(); $queryEndTime = microtime(true); $executionMs = (int) round(($queryEndTime - $queryStartTime) * 1000); // Log the query for NL2SQL training $userQueryDesc = buildUserQueryDescription( $searchType, $searchName, $selectedTags, $startDate, $endDate, $limit, $timeInterval ); $logParams = $params; unset($logParams[':invalid_timestamp']); // Remove internal filter param logControlsQuery( $userQueryDesc, $sql, $logParams, 'opcsearch', $executionMs, count($results) ); foreach ($results as $row) { $timestamp = $row['TimeStamp']; $tagName = $row['name']; $value = $row['Value']; if (!isset($organizedResults[$timestamp])) { $organizedResults[$timestamp] = ['timestamp' => $timestamp]; } $organizedResults[$timestamp][$tagName] = $value; if (!in_array($tagName, $tagNames, true)) { $tagNames[] = $tagName; } } if (!empty($organizedResults)) { if ($showTrend && $searchType === 'single_name') { ksort($organizedResults); } else { krsort($organizedResults); } } } catch (PDOException $e) { echo '
Query error: ' . htmlspecialchars($e->getMessage()) . '
'; } } function formatTimestamp($timestamp) { return date('Y-m-d H:i:s', strtotime($timestamp)); } function formatValue($value) { if (is_numeric($value)) { return number_format((float) $value, 2); } return $value; } $chartData = []; $chartLabels = []; if (!empty($results) && $showTrend && $searchType === 'single_name') { foreach ($results as $row) { $chartLabels[] = date('M j, Y H:i', strtotime($row['TimeStamp'])); $chartData[] = is_numeric($row['Value']) ? (float) $row['Value'] : 0; } } $pageTitle = 'Historian Search'; $pageSubtitle = 'Query the SQL historian and visualize trends.'; $pageDescription = 'Search LASUCA historian data by tag, filter results, and export charts.'; $layoutWithoutSidebar = true; $layoutReturnUrl = 'overview.php'; $layoutReturnLabel = 'Back to overview'; require __DIR__ . '/includes/layout/header.php'; ?>

Historical data search

Query historian tags by name, filter by time range, and export results or trend charts.

Search for historian data by tag name with autocomplete.

Compare up to five tags side by side.

Sampling interval controls how frequently data points are included in results.

>
Reset
Showing timestamp(s) and data point(s) for 1) : ?>
Interval: = 60) { $minutes = $timeInterval / 60; echo $minutes . ' minute' . ($minutes > 1 ? 's' : ''); } else { echo $timeInterval . ' second' . ($timeInterval > 1 ? 's' : ''); } ?>
$limit) : ?>
Showing first rows of total. Narrow the date range or increase the limit for more data.
Average
Minimum
Maximum
= 0 ? '+' : '') . number_format($change, 2); ?> (= 0 ? '+' : '') . number_format($changePercent, 1); ?>%)
Net change

Trend chart for

Timestamp

No results found

No historian data matched the filters provided. Adjust the tag selection or date range and try again.

Please wait…
Searching database for historical data
Searching database…