1003 lines
37 KiB
PHP
1003 lines
37 KiB
PHP
<?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 - Single Chart Real-Time Trend</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>
|
|
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: #2c3e50;
|
|
color: #fff;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.dashboard-header {
|
|
background: #34495e;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-bottom: 3px solid #3498db;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.dashboard-header h1 {
|
|
color: #3498db;
|
|
font-size: 2rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.dashboard-header .subtitle {
|
|
color: #bdc3c7;
|
|
font-size: 1rem;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.container {
|
|
flex: 1;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
.chart-panel {
|
|
background: #ecf0f1;
|
|
border-radius: 5px;
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
|
overflow: hidden;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.chart-header {
|
|
background: #3498db;
|
|
color: white;
|
|
padding: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.chart-title {
|
|
font-weight: bold;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.chart-status {
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 0.9rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.chart-status.running {
|
|
background: #27ae60;
|
|
color: white;
|
|
}
|
|
|
|
.chart-status.stopped {
|
|
background: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
.controls {
|
|
padding: 20px;
|
|
background: #bdc3c7;
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
border-bottom: 2px solid #95a5a6;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
min-width: 150px;
|
|
}
|
|
|
|
.control-group label {
|
|
font-weight: bold;
|
|
font-size: 0.9rem;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
select, button {
|
|
padding: 10px 15px;
|
|
border: 1px solid #95a5a6;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
select {
|
|
background: white;
|
|
color: #2c3e50;
|
|
min-width: 180px;
|
|
}
|
|
|
|
button {
|
|
background: #3498db;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
font-weight: bold;
|
|
min-width: 80px;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background: #2980b9;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
|
}
|
|
|
|
button:disabled {
|
|
background: #95a5a6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
button#startBtn {
|
|
background: #27ae60;
|
|
}
|
|
|
|
button#startBtn:hover:not(:disabled) {
|
|
background: #219a52;
|
|
}
|
|
|
|
button#stopBtn {
|
|
background: #e74c3c;
|
|
}
|
|
|
|
button#stopBtn:hover:not(:disabled) {
|
|
background: #c0392b;
|
|
}
|
|
|
|
.status {
|
|
margin: 20px;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
|
|
.status.connected {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
border: 2px solid #27ae60;
|
|
}
|
|
|
|
.status.disconnected {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
border: 2px solid #e74c3c;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: 10px;
|
|
padding: 20px;
|
|
background: #ecf0f1;
|
|
border-bottom: 2px solid #bdc3c7;
|
|
}
|
|
|
|
.stat-box {
|
|
background: #fff;
|
|
padding: 10px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 2px solid #bdc3c7;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.stat-box:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 15px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
color: #3498db;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-valueSecondary {
|
|
font-size: 1.8rem;
|
|
font-weight: bold;
|
|
color: #27ae60;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.85rem;
|
|
color: #7f8c8d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.chart-container {
|
|
padding: 20px;
|
|
background: white;
|
|
min-height: 500px;
|
|
position: relative;
|
|
}
|
|
|
|
.chart-container canvas {
|
|
width: 100% !important;
|
|
height: 500px !important;
|
|
}
|
|
|
|
.time-window-display {
|
|
padding: 10px 15px;
|
|
background: #f8f9fa;
|
|
border: 2px solid #3498db;
|
|
border-radius: 6px;
|
|
color: #2c3e50;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
min-width: 180px;
|
|
}
|
|
|
|
/* Responsive design */
|
|
@media (max-width: 1200px) {
|
|
.controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.control-group {
|
|
min-width: auto;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.control-group label {
|
|
margin-right: 10px;
|
|
min-width: 120px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.dashboard-header h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.chart-header {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stats {
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.stat-box {
|
|
padding: 15px;
|
|
}
|
|
|
|
.stat-value,
|
|
.stat-valueSecondary {
|
|
font-size: 1.4rem;
|
|
}
|
|
}
|
|
|
|
/* Loading animation */
|
|
.loading {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid #f3f3f3;
|
|
border-top: 3px solid #3498db;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="dashboard-header">
|
|
<h1>LASUCA Controls - Single Chart Trending</h1>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="chart-panel">
|
|
<div class="chart-header">
|
|
<div class="chart-title" id="chartTitle">Real-Time Process Trend</div>
|
|
<div class="chart-status stopped" id="chartStatus">STOPPED</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label for="tagSelect1">Primary Tag:</label>
|
|
<select id="tagSelect1">
|
|
<option value="">Loading tags...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="tagSelect2">Secondary Tag:</label>
|
|
<select id="tagSelect2">
|
|
<option value="">Optional secondary...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="updateInterval">Update Interval:</label>
|
|
<select id="updateInterval">
|
|
<option value="1000">1 Second</option>
|
|
<option value="5000" selected>5 Seconds</option>
|
|
<option value="10000">10 Seconds</option>
|
|
<option value="30000">30 Seconds</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label>Time Window:</label>
|
|
<div class="time-window-display">
|
|
Last 15 Minutes (Sliding)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label> </label>
|
|
<div style="display: flex; gap: 10px;">
|
|
<button id="startBtn" onclick="startRealTime()">Start</button>
|
|
<button id="stopBtn" onclick="stopRealTime()" disabled>Stop</button>
|
|
<button onclick="clearChart()">Clear</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="status" class="status disconnected">
|
|
Disconnected - Select a primary tag and click Start to begin trending
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat-box">
|
|
<div id="currentValue1" class="stat-value">--</div>
|
|
<div class="stat-label" id="tag1Label">Primary Current</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="currentValue2" class="stat-valueSecondary">--</div>
|
|
<div class="stat-label" id="tag2Label">Secondary Current</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="minValue1" class="stat-value">--</div>
|
|
<div class="stat-label">Primary Min</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="maxValue1" class="stat-value">--</div>
|
|
<div class="stat-label">Primary Max</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="minValue2" class="stat-valueSecondary">--</div>
|
|
<div class="stat-label">Secondary Min</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="maxValue2" class="stat-valueSecondary">--</div>
|
|
<div class="stat-label">Secondary Max</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="dataPoints" class="stat-value">0</div>
|
|
<div class="stat-label">Total Points</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div id="lastUpdate" class="stat-value">--</div>
|
|
<div class="stat-label">Last Update</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container">
|
|
<canvas id="realtimeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let chart = null;
|
|
let updateTimer = null;
|
|
let isRunning = false;
|
|
let lastTimestamp1 = null;
|
|
let lastTimestamp2 = null;
|
|
let chartData1 = [];
|
|
let chartData2 = [];
|
|
let timeWindowMinutes = 15;
|
|
|
|
// Initialize Chart.js with dual datasets
|
|
function initChart() {
|
|
const ctx = document.getElementById('realtimeChart').getContext('2d');
|
|
|
|
chart = new Chart(ctx, {
|
|
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: 300
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
time: {
|
|
unit: 'minute',
|
|
displayFormats: {
|
|
second: 'HH:mm:ss',
|
|
minute: 'HH:mm',
|
|
hour: 'HH:mm'
|
|
}
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Time (Last 15 Minutes)',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
min: function() {
|
|
return moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
},
|
|
max: function() {
|
|
return moment().toDate();
|
|
},
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Primary Tag Value',
|
|
color: '#3498db',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
grid: {
|
|
drawOnChartArea: true,
|
|
color: 'rgba(52, 152, 219, 0.1)'
|
|
},
|
|
ticks: {
|
|
color: '#3498db',
|
|
font: { weight: 'bold' }
|
|
}
|
|
},
|
|
y2: {
|
|
type: 'linear',
|
|
display: false,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Secondary Tag Value',
|
|
color: '#27ae60',
|
|
font: { size: 12, weight: 'bold' }
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
ticks: {
|
|
color: '#27ae60',
|
|
font: { weight: 'bold' }
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
font: { size: 12, weight: 'bold' },
|
|
usePointStyle: true,
|
|
padding: 20
|
|
},
|
|
onClick: function(e, legendItem, legend) {
|
|
const index = legendItem.datasetIndex;
|
|
const ci = legend.chart;
|
|
const dataset = ci.data.datasets[index];
|
|
|
|
dataset.hidden = !dataset.hidden;
|
|
|
|
if (index === 1) {
|
|
ci.options.scales.y2.display = !dataset.hidden;
|
|
}
|
|
|
|
ci.update();
|
|
}
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(44, 62, 80, 0.9)',
|
|
titleColor: '#3498db',
|
|
bodyColor: '#ecf0f1',
|
|
borderColor: '#3498db',
|
|
borderWidth: 1,
|
|
titleFont: { size: 13, weight: 'bold' },
|
|
bodyFont: { size: 12 },
|
|
callbacks: {
|
|
title: function(context) {
|
|
return moment(context[0].parsed.x).format('YYYY-MM-DD HH:mm:ss');
|
|
},
|
|
afterTitle: function(context) {
|
|
const now = moment();
|
|
const pointTime = moment(context[0].parsed.x);
|
|
const secondsAgo = now.diff(pointTime, 'seconds');
|
|
return `(${secondsAgo} seconds ago)`;
|
|
},
|
|
label: function(context) {
|
|
const datasetLabel = context.dataset.label;
|
|
const value = context.parsed.y.toFixed(2);
|
|
return `${datasetLabel}: ${value}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}); // This closing brace and parenthesis were missing!
|
|
}
|
|
|
|
// Load available tags for both dropdowns
|
|
async function loadTags() {
|
|
console.log('=== Starting loadTags function ===');
|
|
|
|
const select1 = document.getElementById('tagSelect1');
|
|
const select2 = document.getElementById('tagSelect2');
|
|
|
|
// Check if select elements exist
|
|
console.log('select1 element:', select1);
|
|
console.log('select2 element:', select2);
|
|
|
|
if (!select1 || !select2) {
|
|
console.error('ERROR: Select elements not found!');
|
|
console.log('Available elements with tagSelect IDs:');
|
|
console.log('tagSelect1:', document.getElementById('tagSelect1'));
|
|
console.log('tagSelect2:', document.getElementById('tagSelect2'));
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
select1.innerHTML = '<option value="">Loading tags...</option>';
|
|
select2.innerHTML = '<option value="">Loading tags...</option>';
|
|
console.log('Set loading state');
|
|
|
|
try {
|
|
console.log('Fetching get_tags.php...');
|
|
|
|
const response = await fetch('get_tags.php', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
cache: 'no-cache'
|
|
});
|
|
|
|
console.log('Fetch response:', response);
|
|
console.log('Response status:', response.status);
|
|
console.log('Response ok:', response.ok);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log('Raw response text:', responseText);
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(responseText);
|
|
console.log('Parsed JSON data:', data);
|
|
} catch (parseError) {
|
|
console.error('JSON parse error:', parseError);
|
|
throw new Error('Invalid JSON response');
|
|
}
|
|
|
|
// Reset dropdowns
|
|
console.log('Resetting dropdowns...');
|
|
select1.innerHTML = '<option value="">Select primary tag...</option>';
|
|
select2.innerHTML = '<option value="">Select secondary tag...</option>';
|
|
|
|
if (data.success && data.tags && Array.isArray(data.tags)) {
|
|
console.log(`Processing ${data.tags.length} tags:`, data.tags);
|
|
|
|
if (data.tags.length === 0) {
|
|
console.log('No tags found in response');
|
|
select1.innerHTML = '<option value="">No tags found</option>';
|
|
select2.innerHTML = '<option value="">No tags found</option>';
|
|
return;
|
|
}
|
|
|
|
let processedCount = 0;
|
|
data.tags.forEach((tag, index) => {
|
|
console.log(`Processing tag ${index}:`, tag);
|
|
|
|
if (tag && tag.name && tag.name.trim() !== '') {
|
|
console.log(`Adding tag: "${tag.name}"`);
|
|
|
|
// Create options
|
|
const option1 = document.createElement('option');
|
|
option1.value = tag.name;
|
|
option1.textContent = tag.name;
|
|
|
|
const option2 = document.createElement('option');
|
|
option2.value = tag.name;
|
|
option2.textContent = tag.name;
|
|
|
|
// Add to dropdowns
|
|
select1.appendChild(option1);
|
|
select2.appendChild(option2);
|
|
|
|
processedCount++;
|
|
} else {
|
|
console.log(`Skipping invalid tag:`, tag);
|
|
}
|
|
});
|
|
|
|
console.log(`Successfully processed ${processedCount} tags`);
|
|
console.log('Final select1 options:', select1.options.length);
|
|
console.log('Final select2 options:', select2.options.length);
|
|
|
|
// Debug: List all options
|
|
for (let i = 0; i < select1.options.length; i++) {
|
|
console.log(`Option ${i}: value="${select1.options[i].value}" text="${select1.options[i].textContent}"`);
|
|
}
|
|
|
|
} else {
|
|
console.error('Invalid data structure:', data);
|
|
const errorMsg = data.error || 'Invalid response format';
|
|
select1.innerHTML = `<option value="">Error: ${errorMsg}</option>`;
|
|
select2.innerHTML = `<option value="">Error: ${errorMsg}</option>`;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error in loadTags:', error);
|
|
console.error('Error stack:', error.stack);
|
|
select1.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
|
select2.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
|
}
|
|
|
|
console.log('=== loadTags function complete ===');
|
|
}
|
|
|
|
// Fetch data for one or both tags
|
|
async function fetchTagData(tagName, datasetIndex) {
|
|
if (!tagName) return null;
|
|
|
|
try {
|
|
const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
const startTime = moment().subtract(timeWindowMinutes, 'minutes').format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
let url = `realtime_data.php?tag=${encodeURIComponent(tagName)}&limit=1500&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) {
|
|
const formattedData = data.data.map(point => ({
|
|
x: new Date(point.x),
|
|
y: point.y
|
|
}));
|
|
|
|
const cutoffTime = moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
return formattedData.filter(point => point.x >= cutoffTime);
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error(`Error fetching data for ${tagName}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Modified fetchData for dual tags
|
|
async function fetchData() {
|
|
const tagName1 = document.getElementById('tagSelect1').value;
|
|
const tagName2 = document.getElementById('tagSelect2').value;
|
|
|
|
if (!tagName1) return;
|
|
|
|
try {
|
|
console.log('Fetching data for primary tag:', tagName1);
|
|
const data1 = await fetchTagData(tagName1, 0);
|
|
|
|
if (data1 !== null) {
|
|
chartData1 = data1;
|
|
chart.data.datasets[0].data = chartData1;
|
|
chart.data.datasets[0].label = tagName1;
|
|
}
|
|
|
|
if (tagName2 && tagName2 !== tagName1) {
|
|
console.log('Fetching data for secondary tag:', tagName2);
|
|
const data2 = await fetchTagData(tagName2, 1);
|
|
|
|
if (data2 !== null) {
|
|
chartData2 = data2;
|
|
chart.data.datasets[1].data = chartData2;
|
|
chart.data.datasets[1].label = tagName2;
|
|
chart.data.datasets[1].hidden = false;
|
|
chart.options.scales.y2.display = true;
|
|
}
|
|
} else {
|
|
chartData2 = [];
|
|
chart.data.datasets[1].data = [];
|
|
chart.data.datasets[1].hidden = true;
|
|
chart.options.scales.y2.display = false;
|
|
}
|
|
|
|
chart.options.scales.x.min = moment().subtract(timeWindowMinutes, 'minutes').toDate();
|
|
chart.options.scales.x.max = moment().toDate();
|
|
|
|
chart.update('active');
|
|
|
|
updateStatus('connected', `Connected - Last update: ${new Date().toLocaleTimeString()}`);
|
|
updateStatistics();
|
|
updateChartTitle();
|
|
|
|
} catch (error) {
|
|
console.error('Fetch error:', error);
|
|
updateStatus('disconnected', `Connection error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Update chart title
|
|
function updateChartTitle() {
|
|
const tagName1 = document.getElementById('tagSelect1').value;
|
|
const tagName2 = document.getElementById('tagSelect2').value;
|
|
|
|
let title = 'Real-Time Process Trend';
|
|
if (tagName1) {
|
|
title = tagName1;
|
|
if (tagName2) {
|
|
title += ` & ${tagName2}`;
|
|
}
|
|
}
|
|
|
|
document.getElementById('chartTitle').textContent = title;
|
|
}
|
|
|
|
// Enhanced statistics for dual tags
|
|
function updateStatistics() {
|
|
const tagName1 = document.getElementById('tagSelect1').value;
|
|
const tagName2 = document.getElementById('tagSelect2').value;
|
|
|
|
document.getElementById('tag1Label').textContent = tagName1 ? `${tagName1} Current` : 'Primary Current';
|
|
document.getElementById('tag2Label').textContent = tagName2 ? `${tagName2} Current` : 'Secondary Current';
|
|
|
|
// Primary tag statistics
|
|
if (chartData1.length > 0) {
|
|
const values1 = chartData1.map(point => point.y);
|
|
const current1 = values1[values1.length - 1];
|
|
const min1 = Math.min(...values1);
|
|
const max1 = Math.max(...values1);
|
|
|
|
document.getElementById('currentValue1').textContent = current1.toFixed(2);
|
|
document.getElementById('minValue1').textContent = min1.toFixed(2);
|
|
document.getElementById('maxValue1').textContent = max1.toFixed(2);
|
|
} else {
|
|
document.getElementById('currentValue1').textContent = '--';
|
|
document.getElementById('minValue1').textContent = '--';
|
|
document.getElementById('maxValue1').textContent = '--';
|
|
}
|
|
|
|
// Secondary tag statistics
|
|
if (chartData2.length > 0) {
|
|
const values2 = chartData2.map(point => point.y);
|
|
const current2 = values2[values2.length - 1];
|
|
const min2 = Math.min(...values2);
|
|
const max2 = Math.max(...values2);
|
|
|
|
document.getElementById('currentValue2').textContent = current2.toFixed(2);
|
|
document.getElementById('minValue2').textContent = min2.toFixed(2);
|
|
document.getElementById('maxValue2').textContent = max2.toFixed(2);
|
|
} else {
|
|
document.getElementById('currentValue2').textContent = '--';
|
|
document.getElementById('minValue2').textContent = '--';
|
|
document.getElementById('maxValue2').textContent = '--';
|
|
}
|
|
|
|
const totalPoints = chartData1.length + chartData2.length;
|
|
document.getElementById('dataPoints').textContent = totalPoints;
|
|
document.getElementById('lastUpdate').textContent = moment().format('HH:mm:ss');
|
|
}
|
|
|
|
// Update status display
|
|
function updateStatus(type, message) {
|
|
const status = document.getElementById('status');
|
|
const chartStatus = document.getElementById('chartStatus');
|
|
|
|
status.className = `status ${type}`;
|
|
status.textContent = message;
|
|
|
|
if (type === 'connected') {
|
|
chartStatus.className = 'chart-status running';
|
|
chartStatus.textContent = 'RUNNING';
|
|
} else {
|
|
chartStatus.className = 'chart-status stopped';
|
|
chartStatus.textContent = 'STOPPED';
|
|
}
|
|
}
|
|
|
|
// Start real-time updates
|
|
function startRealTime() {
|
|
const tagName1 = document.getElementById('tagSelect1').value;
|
|
if (!tagName1) {
|
|
alert('Please select a primary tag first');
|
|
return;
|
|
}
|
|
|
|
isRunning = true;
|
|
lastTimestamp1 = null;
|
|
lastTimestamp2 = null;
|
|
|
|
const interval = parseInt(document.getElementById('updateInterval').value);
|
|
|
|
document.getElementById('startBtn').disabled = true;
|
|
document.getElementById('stopBtn').disabled = false;
|
|
document.getElementById('tagSelect1').disabled = true;
|
|
document.getElementById('tagSelect2').disabled = true;
|
|
|
|
fetchData();
|
|
|
|
updateTimer = setInterval(() => {
|
|
fetchData();
|
|
}, interval);
|
|
|
|
const tagName2 = document.getElementById('tagSelect2').value;
|
|
const statusMessage = tagName2 ?
|
|
`Trending ${tagName1} & ${tagName2} - Update every ${interval/1000}s` :
|
|
`Trending ${tagName1} - Update every ${interval/1000}s`;
|
|
|
|
updateStatus('connected', statusMessage);
|
|
}
|
|
|
|
// Stop real-time updates
|
|
function stopRealTime() {
|
|
isRunning = false;
|
|
|
|
if (updateTimer) {
|
|
clearInterval(updateTimer);
|
|
updateTimer = null;
|
|
}
|
|
|
|
document.getElementById('startBtn').disabled = false;
|
|
document.getElementById('stopBtn').disabled = true;
|
|
document.getElementById('tagSelect1').disabled = false;
|
|
document.getElementById('tagSelect2').disabled = false;
|
|
|
|
updateStatus('disconnected', 'Real-time updates stopped - Select tags and click Start to resume');
|
|
}
|
|
|
|
// Clear chart data
|
|
function clearChart() {
|
|
chartData1 = [];
|
|
chartData2 = [];
|
|
lastTimestamp1 = null;
|
|
lastTimestamp2 = null;
|
|
|
|
if (chart) {
|
|
chart.data.datasets[0].data = [];
|
|
chart.data.datasets[1].data = [];
|
|
chart.data.datasets[1].hidden = true;
|
|
chart.options.scales.y2.display = false;
|
|
chart.update();
|
|
}
|
|
|
|
// Reset all statistics
|
|
document.getElementById('currentValue1').textContent = '--';
|
|
document.getElementById('currentValue2').textContent = '--';
|
|
document.getElementById('minValue1').textContent = '--';
|
|
document.getElementById('maxValue1').textContent = '--';
|
|
document.getElementById('minValue2').textContent = '--';
|
|
document.getElementById('maxValue2').textContent = '--';
|
|
document.getElementById('dataPoints').textContent = '0';
|
|
document.getElementById('lastUpdate').textContent = '--';
|
|
|
|
// Reset title
|
|
document.getElementById('chartTitle').textContent = 'Real-Time Process Trend';
|
|
}
|
|
|
|
// Handle interval changes
|
|
document.getElementById('updateInterval').addEventListener('change', function() {
|
|
if (isRunning) {
|
|
stopRealTime();
|
|
setTimeout(startRealTime, 100);
|
|
}
|
|
});
|
|
|
|
// Handle secondary tag changes during runtime
|
|
document.getElementById('tagSelect2').addEventListener('change', function() {
|
|
if (isRunning) {
|
|
fetchData();
|
|
}
|
|
});
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('=== DOMContentLoaded fired ===');
|
|
debugDOM();
|
|
initChart();
|
|
|
|
// Add a small delay to ensure DOM is fully ready
|
|
setTimeout(() => {
|
|
console.log('Starting loadTags after delay...');
|
|
loadTags();
|
|
}, 100);
|
|
});
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', function() {
|
|
if (updateTimer) {
|
|
clearInterval(updateTimer);
|
|
}
|
|
});
|
|
|
|
// Also add the missing debugDOM function:
|
|
function debugDOM() {
|
|
console.log('=== DOM Debug ===');
|
|
console.log('Document ready state:', document.readyState);
|
|
console.log('tagSelect1 element:', document.getElementById('tagSelect1'));
|
|
console.log('tagSelect2 element:', document.getElementById('tagSelect2'));
|
|
|
|
// List all select elements
|
|
const allSelects = document.querySelectorAll('select');
|
|
console.log('All select elements found:', allSelects.length);
|
|
allSelects.forEach((select, index) => {
|
|
console.log(`Select ${index}: id="${select.id}" name="${select.name}"`);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|