add all files

This commit is contained in:
Rucus
2026-02-17 09:29:34 -06:00
parent b8c8d67c67
commit 782d203799
21925 changed files with 2433086 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
-- Composite indexes for your use case
CREATE INDEX idx_historicaldata_compound ON historicaldata (ID, TimeStamp DESC);
CREATE INDEX idx_historicaldata_timestamp ON historicaldata (TimeStamp DESC);
CREATE INDEX idx_id_names_lookup ON id_names (name, idnumber);
-- For date range queries
CREATE INDEX idx_historicaldata_daterange ON historicaldata (TimeStamp, ID)
WHERE TimeStamp >= '2020-01-01';
-- Partial indexes for recent data (if most queries are recent)
CREATE INDEX idx_recent_data ON historicaldata (ID, TimeStamp DESC)
WHERE TimeStamp >= NOW() - INTERVAL 30 DAY;

View File

@@ -0,0 +1,63 @@
<?php
// filepath: v:\controls\trends\live\get_tags.php
header('Content-Type: application/json');
header('Cache-Control: no-cache, must-revalidate');
// Enable error reporting for debugging
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Database connection - same as your other files
$servername = "192.168.0.13\\SQLEXPRESS";
$username = "opce";
$password = "opcelasuca";
$dbname = "history";
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
]
);
$stmt = $pdo->prepare("
SELECT DISTINCT name
FROM dbo.id_names
WHERE name IS NOT NULL AND name <> ''
ORDER BY name
");
$stmt->execute();
$tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Debug: Log the query result
error_log("Found " . count($tags) . " tags");
$response = [
'success' => true,
'tags' => $tags,
'count' => count($tags)
];
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
$response = [
'success' => false,
'error' => 'Database error: ' . $e->getMessage(),
'tags' => []
];
} catch (Exception $e) {
error_log("General error: " . $e->getMessage());
$response = [
'success' => false,
'error' => 'Error: ' . $e->getMessage(),
'tags' => []
];
}
echo json_encode($response);
?>

View File

@@ -0,0 +1,20 @@
<?php
// filepath: v:\controls\trends\live\get_tags_test.php
header('Content-Type: application/json');
// Hardcoded test data
$response = [
'success' => true,
'tags' => [
['name' => 'Test Tag 1'],
['name' => 'Test Tag 2'],
['name' => 'Test Tag 3'],
['name' => 'Mill Speed 1'],
['name' => 'Tank Level A']
],
'count' => 5
];
echo json_encode($response);
?>

View File

@@ -0,0 +1,311 @@
<?php
// filepath: v:\controls\trends\live\index.php
include "../../session.php";
include "../../userAccess.php";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LASUCA Controls - Real-Time Trending Applications</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #2c3e50, #34495e);
color: #ecf0f1;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #34495e;
padding: 20px;
text-align: center;
border-bottom: 3px solid #3498db;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header h1 {
color: #3498db;
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.header p {
font-size: 1.1rem;
color: #bdc3c7;
margin-bottom: 5px;
}
.container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
max-width: 1000px;
width: 100%;
}
.app-card {
background: #ecf0f1;
border-radius: 15px;
padding: 30px;
text-align: center;
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
color: #2c3e50;
position: relative;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 35px rgba(0,0,0,0.4);
}
.app-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.1), transparent);
transition: left 0.5s;
}
.app-card:hover::before {
left: 100%;
}
.app-icon {
font-size: 4rem;
margin-bottom: 20px;
color: #3498db;
}
.app-card h2 {
color: #2c3e50;
font-size: 1.8rem;
margin-bottom: 15px;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.app-description {
color: #7f8c8d;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 25px;
}
.app-features {
text-align: left;
margin-bottom: 25px;
}
.app-features ul {
list-style: none;
color: #5d6d7e;
}
.app-features li {
margin-bottom: 8px;
padding-left: 20px;
position: relative;
}
.app-features li::before {
content: '✓';
position: absolute;
left: 0;
color: #27ae60;
font-weight: bold;
}
.app-link {
display: inline-block;
background: #3498db;
color: white;
text-decoration: none;
padding: 15px 30px;
border-radius: 8px;
font-weight: bold;
font-size: 1.1rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
}
.app-link:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
.beta-badge {
position: absolute;
top: 15px;
right: 15px;
background: #e67e22;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.feedback-section {
background: #34495e;
padding: 30px 20px;
text-align: center;
border-top: 2px solid #3498db;
margin-top: auto;
}
.feedback-section h3 {
color: #3498db;
font-size: 1.5rem;
margin-bottom: 15px;
}
.feedback-section p {
color: #bdc3c7;
font-size: 1rem;
max-width: 600px;
margin: 0 auto 20px;
line-height: 1.6;
}
.feedback-contact {
color: #3498db;
font-weight: bold;
text-decoration: none;
}
.feedback-contact:hover {
color: #5dade2;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;
}
.app-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.app-card {
padding: 25px;
}
.app-icon {
font-size: 3rem;
}
}
</style>
</head>
<body>
<div class="header">
<h1>LASUCA Controls</h1>
<p>Real-Time Process Trending Applications</p>
<p><strong>Testing Environment - Beta Version</strong></p>
</div>
<div class="container">
<div class="app-grid">
<!-- Single Chart Application -->
<div class="app-card">
<div class="beta-badge">Beta</div>
<div class="app-icon">📊</div>
<h2>Single Chart Trending</h2>
<div class="app-description">
A focused real-time trending application for monitoring one or two process variables with detailed analysis and multiple time windows.
</div>
<div class="app-features">
<ul>
<li>Single chart with dual Y-axes</li>
<li>Primary + Secondary tag support</li>
<li>Real-time statistics display</li>
<li>Sliding time window</li>
</ul>
</div>
<a href="singlechart.php" class="app-link">Launch Single Chart</a>
</div>
<!-- Multi-Chart Application -->
<div class="app-card">
<div class="beta-badge">Beta</div>
<div class="app-icon">📈</div>
<h2>Multi-Chart Dashboard</h2>
<div class="app-description">
A comprehensive dashboard for monitoring multiple process variables simultaneously with independent controls for each chart panel.
</div>
<div class="app-features">
<ul>
<li>4 independent chart panels</li>
<li>Dual tags per chart (8 total)</li>
<li>Global and individual controls</li>
<li>Full-screen dashboard layout</li>
<li>Synchronized time windows</li>
<li>10, 15, and 30-minute time windows</li>
</ul>
</div>
<a href="multichart.php" class="app-link">Launch Multi-Chart</a>
</div>
</div>
</div>
<div class="feedback-section">
<h3>🔧 Testing & Feedback</h3>
<p>
<strong>Please report:</strong> Bugs, performance issues, feature requests, or general usability feedback.
</p>
</div>
<script>
// Add some interactive effects
document.addEventListener('DOMContentLoaded', function() {
const cards = document.querySelectorAll('.app-card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-10px) scale(1.02)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0) scale(1)';
});
});
// Add click tracking for analytics (optional)
const links = document.querySelectorAll('.app-link');
links.forEach(link => {
link.addEventListener('click', function(e) {
const appName = this.textContent.trim();
console.log(`User clicked: ${appName} at ${new Date().toISOString()}`);
// You could send this data to analytics if needed
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,944 @@
<?php include "../../session.php";
include "../../userAccess.php"; ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LASUCA Controls - Multi-Chart Real-Time Dashboard</title>
<!-- Chart.js imports -->
<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>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.css" />
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #2c3e50;
color: #fff;
overflow-x: hidden;
}
.dashboard-header {
background: #34495e;
padding: 15px 20px;
border-bottom: 2px solid #3498db;
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.dashboard-header h1 {
color: #3498db;
font-size: 2rem;
margin: 0;
text-align: center;
}
.global-controls {
display: flex;
justify-content: center;
gap: 20px;
padding: 15px;
background: #34495e;
border-bottom: 1px solid #3498db;
}
.global-control-group {
display: flex;
align-items: center;
gap: 10px;
}
.global-control-group label {
font-weight: bold;
color: #ecf0f1;
}
.dashboard-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 15px;
padding: 15px;
height: calc(100vh - 140px);
min-height: 600px;
}
.chart-panel {
background: #ecf0f1;
border-radius: 5px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.chart-header {
background: #3498db;
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 50px;
}
.chart-title {
font-weight: bold;
font-size: 1.1rem;
}
.chart-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.chart-status.running {
background: #27ae60;
color: white;
}
.chart-status.stopped {
background: #e74c3c;
color: white;
}
.chart-controls {
padding: 12px 15px;
background: #bdc3c7;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
border-bottom: 1px solid #95a5a6;
}
.control-group {
display: flex;
align-items: center;
gap: 5px;
}
.control-group label {
font-size: 0.85rem;
font-weight: bold;
color: #2c3e50;
min-width: 60px;
}
select, button {
padding: 6px 10px;
border: 1px solid #95a5a6;
border-radius: 4px;
font-size: 0.85rem;
}
select {
background: white;
min-width: 140px;
}
button {
background: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s;
}
button:hover:not(:disabled) {
background: #2980b9;
}
button:disabled {
background: #95a5a6;
cursor: not-allowed;
}
button.start-btn {
background: #27ae60;
}
button.start-btn:hover:not(:disabled) {
background: #219a52;
}
button.stop-btn {
background: #e74c3c;
}
button.stop-btn:hover:not(:disabled) {
background: #c0392b;
}
.chart-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 8px;
padding: 10px 15px;
background: #ecf0f1;
border-bottom: 1px solid #bdc3c7;
}
.stat-box {
background: #fff;
padding: 8px;
border-radius: 6px;
text-align: center;
border: 1px solid #bdc3c7;
}
.stat-value {
font-size: 1rem;
font-weight: bold;
color: #3498db;
}
.stat-value-secondary {
font-size: 1rem;
font-weight: bold;
color: #27ae60;
}
.stat-label {
font-size: 0.7rem;
color: #7f8c8d;
margin-top: 2px;
}
.chart-container {
flex: 1;
padding: 10px;
background: white;
position: relative;
min-height: 200px;
}
.chart-container canvas {
width: 100% !important;
height: 100% !important;
}
/* Global control styling */
.global-controls select,
.global-controls button {
padding: 8px 12px;
font-size: 0.9rem;
}
.global-controls button {
background: #e67e22;
font-weight: bold;
}
.global-controls button:hover {
background: #d35400;
}
/* Responsive design */
@media (max-width: 1200px) {
.dashboard-container {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, 1fr);
height: auto;
min-height: 2400px;
}
.chart-panel {
min-height: 500px;
}
}
@media (max-width: 768px) {
.chart-controls {
flex-direction: column;
align-items: stretch;
}
.control-group {
justify-content: space-between;
}
.global-controls {
flex-direction: column;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="dashboard-header">
<h1>LASUCA Controls - Real-Time Multi-Chart Dashboard</h1>
</div>
<div class="global-controls">
<div class="global-control-group">
<label>Update Interval:</label>
<select id="globalUpdateInterval">
<option value="5000" selected>5 Seconds</option>
<option value="10000">10 Seconds</option>
<option value="30000">30 Seconds</option>
</select>
</div>
<div class="global-control-group">
<label>Time Window:</label>
<select id="globalTimeWindow">
<option value="10">10 Minutes</option>
<option value="15" selected>15 Minutes</option>
<option value="30">30 Minutes</option>
<option value="60">1 Hour</option>
<!--<option value="60">60 Minutes</option>-->
</select>
</div>
<button onclick="startAllCharts()">Start All Charts</button>
<button onclick="stopAllCharts()">Stop All Charts</button>
<button onclick="clearAllCharts()">Clear All Charts</button>
</div>
<div class="dashboard-container">
<!-- Chart 1 -->
<div class="chart-panel" id="panel1">
<div class="chart-header">
<div class="chart-title" id="title1">Chart 1</div>
<div class="chart-status stopped" id="status1">STOPPED</div>
</div>
<div class="chart-controls">
<div class="control-group">
<label>Primary:</label>
<select id="tagSelect1_1">
<option value="">Loading...</option>
</select>
</div>
<div class="control-group">
<label>Secondary:</label>
<select id="tagSelect1_2">
<option value="">Optional...</option>
</select>
</div>
<button class="start-btn" onclick="startChart(1)">Start</button>
<button class="stop-btn" onclick="stopChart(1)">Stop</button>
<button onclick="clearChart(1)">Clear</button>
</div>
<div class="chart-stats">
<div class="stat-box">
<div id="current1_1" class="stat-value">--</div>
<div class="stat-label">Current 1</div>
</div>
<div class="stat-box">
<div id="current1_2" class="stat-value-secondary">--</div>
<div class="stat-label">Current 2</div>
</div>
<div class="stat-box">
<div id="points1" class="stat-value">0</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-box">
<div id="update1" class="stat-value">--</div>
<div class="stat-label">Updated</div>
</div>
</div>
<div class="chart-container">
<canvas id="chart1"></canvas>
</div>
</div>
<!-- Chart 2 -->
<div class="chart-panel" id="panel2">
<div class="chart-header">
<div class="chart-title" id="title2">Chart 2</div>
<div class="chart-status stopped" id="status2">STOPPED</div>
</div>
<div class="chart-controls">
<div class="control-group">
<label>Primary:</label>
<select id="tagSelect2_1">
<option value="">Loading...</option>
</select>
</div>
<div class="control-group">
<label>Secondary:</label>
<select id="tagSelect2_2">
<option value="">Optional...</option>
</select>
</div>
<button class="start-btn" onclick="startChart(2)">Start</button>
<button class="stop-btn" onclick="stopChart(2)">Stop</button>
<button onclick="clearChart(2)">Clear</button>
</div>
<div class="chart-stats">
<div class="stat-box">
<div id="current2_1" class="stat-value">--</div>
<div class="stat-label">Current 1</div>
</div>
<div class="stat-box">
<div id="current2_2" class="stat-value-secondary">--</div>
<div class="stat-label">Current 2</div>
</div>
<div class="stat-box">
<div id="points2" class="stat-value">0</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-box">
<div id="update2" class="stat-value">--</div>
<div class="stat-label">Updated</div>
</div>
</div>
<div class="chart-container">
<canvas id="chart2"></canvas>
</div>
</div>
<!-- Chart 3 -->
<div class="chart-panel" id="panel3">
<div class="chart-header">
<div class="chart-title" id="title3">Chart 3</div>
<div class="chart-status stopped" id="status3">STOPPED</div>
</div>
<div class="chart-controls">
<div class="control-group">
<label>Primary:</label>
<select id="tagSelect3_1">
<option value="">Loading...</option>
</select>
</div>
<div class="control-group">
<label>Secondary:</label>
<select id="tagSelect3_2">
<option value="">Optional...</option>
</select>
</div>
<button class="start-btn" onclick="startChart(3)">Start</button>
<button class="stop-btn" onclick="stopChart(3)">Stop</button>
<button onclick="clearChart(3)">Clear</button>
</div>
<div class="chart-stats">
<div class="stat-box">
<div id="current3_1" class="stat-value">--</div>
<div class="stat-label">Current 1</div>
</div>
<div class="stat-box">
<div id="current3_2" class="stat-value-secondary">--</div>
<div class="stat-label">Current 2</div>
</div>
<div class="stat-box">
<div id="points3" class="stat-value">0</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-box">
<div id="update3" class="stat-value">--</div>
<div class="stat-label">Updated</div>
</div>
</div>
<div class="chart-container">
<canvas id="chart3"></canvas>
</div>
</div>
<!-- Chart 4 -->
<div class="chart-panel" id="panel4">
<div class="chart-header">
<div class="chart-title" id="title4">Chart 4</div>
<div class="chart-status stopped" id="status4">STOPPED</div>
</div>
<div class="chart-controls">
<div class="control-group">
<label>Primary:</label>
<select id="tagSelect4_1">
<option value="">Loading...</option>
</select>
</div>
<div class="control-group">
<label>Secondary:</label>
<select id="tagSelect4_2">
<option value="">Optional...</option>
</select>
</div>
<button class="start-btn" onclick="startChart(4)">Start</button>
<button class="stop-btn" onclick="stopChart(4)">Stop</button>
<button onclick="clearChart(4)">Clear</button>
</div>
<div class="chart-stats">
<div class="stat-box">
<div id="current4_1" class="stat-value">--</div>
<div class="stat-label">Current 1</div>
</div>
<div class="stat-box">
<div id="current4_2" class="stat-value-secondary">--</div>
<div class="stat-label">Current 2</div>
</div>
<div class="stat-box">
<div id="points4" class="stat-value">0</div>
<div class="stat-label">Points</div>
</div>
<div class="stat-box">
<div id="update4" class="stat-value">--</div>
<div class="stat-label">Updated</div>
</div>
</div>
<div class="chart-container">
<canvas id="chart4"></canvas>
</div>
</div>
</div>
<script>
// Global variables for multi-chart management
const charts = {};
const chartData = {};
const updateTimers = {};
const chartStates = {};
let globalTimeWindow = 15;
let globalUpdateInterval = 5000;
let availableTags = [];
// Chart configuration template
function getChartConfig(chartId) {
return {
type: 'line',
data: {
datasets: [{
label: 'Primary Tag',
data: [],
borderColor: '#3498db',
backgroundColor: 'rgba(52, 152, 219, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 0,
hitRadius: 0,
yAxisID: 'y1'
}, {
label: 'Secondary Tag',
data: [],
borderColor: '#27ae60',
backgroundColor: 'rgba(39, 174, 96, 0.1)',
borderWidth: 2,
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 0,
hitRadius: 0,
yAxisID: 'y2',
hidden: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 200
},
interaction: {
intersect: false,
mode: 'index'
},
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
displayFormats: {
second: 'HH:mm:ss',
minute: 'HH:mm'
}
},
title: {
display: true,
text: `Last ${globalTimeWindow} Minutes`,
font: { size: 10 }
},
min: function() {
return moment().subtract(globalTimeWindow, 'minutes').toDate();
},
max: function() {
return moment().toDate();
},
ticks: {
font: { size: 9 }
}
},
y1: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Primary',
color: '#3498db',
font: { size: 10 }
},
grid: {
drawOnChartArea: true,
},
ticks: {
color: '#3498db',
font: { size: 9 }
}
},
y2: {
type: 'linear',
display: false,
position: 'right',
title: {
display: true,
text: 'Secondary',
color: '#27ae60',
font: { size: 10 }
},
grid: {
drawOnChartArea: false,
},
ticks: {
color: '#27ae60',
font: { size: 9 }
}
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: { size: 10 },
usePointStyle: true
}
},
tooltip: {
titleFont: { size: 11 },
bodyFont: { size: 10 },
callbacks: {
title: function(context) {
return moment(context[0].parsed.x).format('HH:mm:ss');
},
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`;
}
}
},
zoom: {
pan: {
enabled: true,
modifierKey: 'ctrl',
mode: 'x'
},
zoom: {
wheel: { enabled: true, modifierKey: 'shift' },
pinch: { enabled: true },
mode: 'x'
}
}
}
}
};
}
// Initialize all charts
function initializeCharts() {
for (let i = 1; i <= 4; i++) {
const ctx = document.getElementById(`chart${i}`).getContext('2d');
charts[i] = new Chart(ctx, getChartConfig(i));
chartData[i] = { primary: [], secondary: [] };
chartStates[i] = { running: false, timer: null };
}
}
// Load tags for all dropdowns
async function loadTags() {
console.log('Loading tags for all charts...');
try {
const response = await fetch('get_tags.php');
const data = await response.json();
if (data.success && data.tags) {
availableTags = data.tags;
// Populate all select boxes
for (let i = 1; i <= 4; i++) {
const select1 = document.getElementById(`tagSelect${i}_1`);
const select2 = document.getElementById(`tagSelect${i}_2`);
select1.innerHTML = '<option value="">Select primary tag...</option>';
select2.innerHTML = '<option value="">Select secondary tag...</option>';
availableTags.forEach(tag => {
const option1 = document.createElement('option');
option1.value = tag.name;
option1.textContent = tag.name;
select1.appendChild(option1);
const option2 = document.createElement('option');
option2.value = tag.name;
option2.textContent = tag.name;
select2.appendChild(option2);
});
}
console.log('Tags loaded for all charts');
} else {
console.error('Failed to load tags');
}
} catch (error) {
console.error('Error loading tags:', error);
}
}
// Fetch data for a specific chart
async function fetchChartData(chartId) {
const tag1 = document.getElementById(`tagSelect${chartId}_1`).value;
const tag2 = document.getElementById(`tagSelect${chartId}_2`).value;
if (!tag1) return;
try {
const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
const startTime = moment().subtract(globalTimeWindow, 'minutes').format('YYYY-MM-DD HH:mm:ss');
// Fetch primary tag data
const data1 = await fetchTagData(tag1);
if (data1) {
chartData[chartId].primary = data1;
charts[chartId].data.datasets[0].data = data1;
charts[chartId].data.datasets[0].label = tag1;
}
// Fetch secondary tag data if selected
if (tag2 && tag2 !== tag1) {
const data2 = await fetchTagData(tag2);
if (data2) {
chartData[chartId].secondary = data2;
charts[chartId].data.datasets[1].data = data2;
charts[chartId].data.datasets[1].label = tag2;
charts[chartId].data.datasets[1].hidden = false;
charts[chartId].options.scales.y2.display = true;
}
} else {
chartData[chartId].secondary = [];
charts[chartId].data.datasets[1].data = [];
charts[chartId].data.datasets[1].hidden = true;
charts[chartId].options.scales.y2.display = false;
}
// Update time axis
charts[chartId].options.scales.x.min = moment().subtract(globalTimeWindow, 'minutes').toDate();
charts[chartId].options.scales.x.max = moment().toDate();
charts[chartId].options.scales.x.title.text = `Last ${globalTimeWindow} Minutes`;
charts[chartId].update('none');
updateChartStats(chartId);
updateChartTitle(chartId);
} catch (error) {
console.error(`Error fetching data for chart ${chartId}:`, error);
}
}
// Fetch data for individual tag
async function fetchTagData(tagName) {
try {
const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
const startTime = moment().subtract(globalTimeWindow, 'minutes').format('YYYY-MM-DD HH:mm:ss');
const url = `realtime_data.php?tag=${encodeURIComponent(tagName)}&limit=7200&start_time=${encodeURIComponent(startTime)}&end_time=${encodeURIComponent(endTime)}`;
const response = await fetch(url);
const data = await response.json();
if (data.success && data.data && data.data.length > 0) {
return data.data.map(point => ({
x: new Date(point.x),
y: point.y
}));
}
return [];
} catch (error) {
console.error(`Error fetching data for ${tagName}:`, error);
return [];
}
}
// Update chart statistics
function updateChartStats(chartId) {
const primary = chartData[chartId].primary;
const secondary = chartData[chartId].secondary;
// Primary tag stats
if (primary.length > 0) {
const current = primary[primary.length - 1].y;
document.getElementById(`current${chartId}_1`).textContent = current.toFixed(2);
} else {
document.getElementById(`current${chartId}_1`).textContent = '--';
}
// Secondary tag stats
if (secondary.length > 0) {
const current = secondary[secondary.length - 1].y;
document.getElementById(`current${chartId}_2`).textContent = current.toFixed(2);
} else {
document.getElementById(`current${chartId}_2`).textContent = '--';
}
// General stats
document.getElementById(`points${chartId}`).textContent = primary.length + secondary.length;
document.getElementById(`update${chartId}`).textContent = moment().format('HH:mm:ss');
}
// Update chart title
function updateChartTitle(chartId) {
const tag1 = document.getElementById(`tagSelect${chartId}_1`).value;
const tag2 = document.getElementById(`tagSelect${chartId}_2`).value;
let title = `Chart ${chartId}`;
if (tag1) {
title += ` - ${tag1}`;
if (tag2) {
title += ` & ${tag2}`;
}
}
document.getElementById(`title${chartId}`).textContent = title;
}
// Start individual chart
function startChart(chartId) {
const tag1 = document.getElementById(`tagSelect${chartId}_1`).value;
if (!tag1) {
alert(`Please select a primary tag for Chart ${chartId}`);
return;
}
if (chartStates[chartId].running) return;
chartStates[chartId].running = true;
document.getElementById(`status${chartId}`).textContent = 'RUNNING';
document.getElementById(`status${chartId}`).className = 'chart-status running';
// Initial fetch
fetchChartData(chartId);
// Start timer
chartStates[chartId].timer = setInterval(() => {
fetchChartData(chartId);
}, globalUpdateInterval);
console.log(`Started Chart ${chartId}`);
}
// Stop individual chart
function stopChart(chartId) {
if (!chartStates[chartId].running) return;
chartStates[chartId].running = false;
document.getElementById(`status${chartId}`).textContent = 'STOPPED';
document.getElementById(`status${chartId}`).className = 'chart-status stopped';
if (chartStates[chartId].timer) {
clearInterval(chartStates[chartId].timer);
chartStates[chartId].timer = null;
}
console.log(`Stopped Chart ${chartId}`);
}
// Clear individual chart
function clearChart(chartId) {
stopChart(chartId);
chartData[chartId] = { primary: [], secondary: [] };
charts[chartId].data.datasets[0].data = [];
charts[chartId].data.datasets[1].data = [];
charts[chartId].data.datasets[1].hidden = true;
charts[chartId].options.scales.y2.display = false;
charts[chartId].update();
// Reset stats
document.getElementById(`current${chartId}_1`).textContent = '--';
document.getElementById(`current${chartId}_2`).textContent = '--';
document.getElementById(`points${chartId}`).textContent = '0';
document.getElementById(`update${chartId}`).textContent = '--';
// Reset title
document.getElementById(`title${chartId}`).textContent = `Chart ${chartId}`;
}
// Global controls
function startAllCharts() {
for (let i = 1; i <= 4; i++) {
const tag1 = document.getElementById(`tagSelect${i}_1`).value;
if (tag1) {
startChart(i);
}
}
}
function stopAllCharts() {
for (let i = 1; i <= 4; i++) {
stopChart(i);
}
}
function clearAllCharts() {
for (let i = 1; i <= 4; i++) {
clearChart(i);
}
}
// Handle global setting changes
document.getElementById('globalUpdateInterval').addEventListener('change', function() {
globalUpdateInterval = parseInt(this.value);
console.log(`Global update interval changed to ${globalUpdateInterval}ms`);
// Update all running charts
for (let i = 1; i <= 4; i++) {
if (chartStates[i].running) {
stopChart(i);
setTimeout(() => startChart(i), 100);
}
}
});
document.getElementById('globalTimeWindow').addEventListener('change', function() {
globalTimeWindow = parseInt(this.value);
console.log(`Global time window changed to ${globalTimeWindow} minutes`);
// Update all charts
for (let i = 1; i <= 4; i++) {
charts[i].options.scales.x.title.text = `Last ${globalTimeWindow} Minutes`;
if (chartStates[i].running) {
fetchChartData(i);
}
}
});
// Initialize everything on page load
document.addEventListener('DOMContentLoaded', function() {
initializeCharts();
loadTags();
console.log('Multi-chart dashboard initialized');
Sortable.create(document.querySelector('.dashboard-container'), {
animation: 150,
handle: '.chart-header',
draggable: '.chart-panel'
});
});
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
stopAllCharts();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
<?php
header('Content-Type: application/json');
header('Cache-Control: no-cache, must-revalidate');
$servername = "192.168.0.13\\SQLEXPRESS";
$username = "opce";
$password = "opcelasuca";
$dbname = "history";
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
]
);
// Get parameters
$tagName = $_GET['tag'] ?? '';
$lastTimestamp = $_GET['last_timestamp'] ?? null;
$limit = min((int)($_GET['limit'] ?? 500), 7200); // Max 10000 points
$startTime = $_GET['start_time'] ?? null;
$endTime = $_GET['end_time'] ?? null;
if (empty($tagName)) {
throw new Exception('Tag name required');
}
// Build query to get latest data from archive
$sql = "SELECT a.TimeStamp, a.Value, a.ID
FROM dbo.archive a
LEFT JOIN dbo.id_names n ON a.ID = n.idnumber
WHERE n.name = :tag_name";
$params = [':tag_name' => $tagName];
if ($startTime && $endTime) {
$sql .= " AND a.TimeStamp BETWEEN :start_time AND :end_time";
$params[':start_time'] = $startTime;
$params[':end_time'] = $endTime;
} elseif ($lastTimestamp) {
$sql .= " AND a.TimeStamp > :last_timestamp";
$params[':last_timestamp'] = $lastTimestamp;
}
$sql .= " ORDER BY a.TimeStamp DESC OFFSET 0 ROWS FETCH NEXT :limit ROWS ONLY";
$stmt = $pdo->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$results = $stmt->fetchAll();
// Reverse to get chronological order
$results = array_reverse($results);
// Format data for Chart.js
$data = [];
foreach ($results as $row) {
$data[] = [
'x' => $row['TimeStamp'],
'y' => (float)$row['Value'],
'timestamp' => $row['TimeStamp']
];
}
$response = [
'success' => true,
'data' => $data,
'count' => count($data),
'latest_timestamp' => !empty($data) ? end($data)['timestamp'] : null,
'tag' => $tagName
];
} catch (Exception $e) {
$response = [
'success' => false,
'error' => $e->getMessage()
];
}
echo json_encode($response);
?>

View File

@@ -0,0 +1,429 @@
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #555555;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: rgb(192, 192, 192);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
.search-type-tabs {
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.tab {
display: inline-block;
padding: 10px 20px;
margin-right: 5px;
background-color: #f1f1f1;
border: 1px solid #ddd;
border-bottom: none;
cursor: pointer;
border-radius: 5px 5px 0 0;
}
.tab.active {
background-color: #007cba;
color: white;
border-color: #007cba;
}
.search-option {
display: none;
padding: 15px;
border: 1px solid #ddd;
border-radius: 0 5px 5px 5px;
background-color: white;
}
.search-option.active {
display: block;
}
.form-row {
margin-bottom: 10px;
display: flex;
align-items: center;
}
.form-row.checkbox {
align-items: flex-start;
}
label {
display: inline-block;
width: 120px;
font-weight: bold;
}
label.checkbox {
width: auto;
margin-left: 5px;
}
input[type="text"], input[type="datetime-local"], select, textarea {
width: 200px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
textarea {
width: 300px;
height: 60px;
resize: vertical;
}
input[type="checkbox"] {
width: auto;
margin: 0;
}
/* Autocomplete styling */
.autocomplete-container {
position: relative;
display: inline-block;
}
.autocomplete-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.autocomplete-suggestion {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
user-select: none;
color: #333;
}
.autocomplete-suggestion:hover,
.autocomplete-suggestion.active {
background-color: #f0f8ff;
color: #007cba;
}
.autocomplete-suggestion:last-child {
border-bottom: none;
}
/* Fix for textarea autocomplete container */
.autocomplete-container textarea {
width: 300px;
height: 60px;
resize: vertical;
}
/* Ensure suggestions dropdown matches textarea width */
.autocomplete-container:has(textarea) .autocomplete-suggestions {
width: 300px;
}
button {
padding: 8px 15px;
margin-right: 10px;
background-color: #007cba;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #005a87;
}
button[type="button"] {
background-color: #6c757d;
}
button[type="button"]:hover {
background-color: #545b62;
}
.trend-controls {
margin: 20px 0;
padding: 15px;
background-color: #e9ecef;
border-radius: 5px;
border: 1px solid #ccc;
}
.chart-container {
margin: 20px 0;
padding: 20px;
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-wrapper {
position: relative;
height: 400px;
margin-bottom: 20px;
}
.stats-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-box {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
border-left: 4px solid #007cba;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007cba;
}
.stat-label {
font-size: 14px;
color: #666;
}
table {
border-collapse: collapse;
width: 100%;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
th.timestamp-header {
background-color: #007cba;
color: white;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f0f8ff;
}
.no-results {
text-align: center;
color: #666;
margin-top: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
}
.results-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding: 10px;
background-color: #e9ecef;
border-radius: 4px;
}
.timestamp {
font-family: monospace;
font-weight: bold;
}
.numeric-value {
text-align: right;
font-family: monospace;
}
.export-buttons {
margin: 10px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.export-buttons button {
background-color: #28a745;
margin-right: 10px;
padding: 8px 15px;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
font-size: 14px;
}
.export-buttons button:hover {
background-color: #218838;
}
.export-buttons button:last-child {
margin-right: 0;
}
.help-text {
font-size: 12px;
color: #666;
font-style: italic;
margin-top: 5px;
}
.table-scroll {
overflow-x: auto;
}
select {
width: 250px; /* Wider for tag names */
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: white;
font-size: 14px;
}
select:focus {
border-color: #007cba;
outline: none;
box-shadow: 0 0 5px rgba(0, 124, 186, 0.3);
}
/* Remove autocomplete styling for multiple names since we're not using it */
#multiple_names .autocomplete-container {
display: none;
}
.form-row small {
color: #666;
font-style: italic;
margin-left: 10px;
}
.interval-info {
font-size: 12px;
color: #666;
margin-top: 2px;
}
/* Loading indicator styles */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
text-align: center;
min-width: 300px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007cba;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.loading-subtext {
font-size: 12px;
color: #666;
font-style: italic;
}
/* Progress bar styles */
.progress-bar {
width: 100%;
height: 6px;
background-color: #f3f3f3;
border-radius: 3px;
overflow: hidden;
margin-top: 15px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007cba, #005a87);
width: 0%;
border-radius: 3px;
transition: width 0.3s ease;
animation: progressPulse 2s ease-in-out infinite;
}
@keyframes progressPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Button loading state */
button[type="submit"]:disabled {
background-color: #6c757d;
cursor: not-allowed;
position: relative;
}
button[type="submit"]:disabled::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
margin: auto;
border: 2px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
top: 0;
bottom: 0;
right: 10px;
}
/* Inline search status */
.search-status {
margin: 15px 0;
padding: 12px;
background-color: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 4px;
text-align: center;
}
.status-content {
display: flex;
align-items: center;
justify-content: center;
}
.mini-spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #007cba;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
margin-right: 10px;
}
.status-text {
color: #007cba;
font-weight: 500;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
<?php
require_once 'vendor/autoload.php'; // Install ReactPHP via Composer
use React\Socket\Server;
use React\Http\Server as HttpServer;
use React\Stream\WritableResourceStream;
$loop = React\EventLoop\Loop::get();
// Database connection
$pdo = new PDO("mysql:host=192.168.0.13;dbname=history", "opce", "opcelasuca");
// WebSocket connections
$connections = [];
// Create WebSocket server
$server = new HttpServer($loop, function ($request) use (&$connections, $pdo) {
// Handle WebSocket upgrade
if ($request->getHeaderLine('Upgrade') === 'websocket') {
$connection = new WebSocketConnection();
$connections[] = $connection;
// Send initial data
$connection->send(json_encode([
'type' => 'welcome',
'message' => 'Connected to real-time data stream'
]));
return $connection;
}
return new React\Http\Response(404, [], 'WebSocket endpoint only');
});
// Periodic data broadcast
$loop->addPeriodicTimer(1.0, function() use (&$connections, $pdo) {
if (empty($connections)) return;
try {
// Get latest data for all active tags
$stmt = $pdo->prepare("
SELECT h.TimeStamp, h.Value, n.name
FROM historicaldata h
LEFT JOIN id_names n ON h.ID = n.idnumber
WHERE h.TimeStamp >= NOW() - INTERVAL 5 SECOND
ORDER BY h.TimeStamp DESC
");
$stmt->execute();
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($data)) {
$message = json_encode([
'type' => 'data_update',
'timestamp' => date('c'),
'data' => $data
]);
// Broadcast to all connections
foreach ($connections as $key => $connection) {
if ($connection->isConnected()) {
$connection->send($message);
} else {
unset($connections[$key]);
}
}
}
} catch (Exception $e) {
error_log("WebSocket error: " . $e->getMessage());
}
});
$socket = new Server('0.0.0.0:8080', $loop);
$server->listen($socket);
echo "WebSocket server running on port 8080\n";
$loop->run();
?>