952 lines
34 KiB
HTML
952 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PLC S7-315 Streamer & Logger</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||
min-height: 100vh;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
color: white;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 10px;
|
||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.header-logo {
|
||
height: 60px;
|
||
width: auto;
|
||
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
|
||
}
|
||
|
||
.status-bar {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.status-item {
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-connected {
|
||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||
color: white;
|
||
}
|
||
|
||
.status-disconnected {
|
||
background: linear-gradient(135deg, #9ca3af, #6b7280);
|
||
color: white;
|
||
}
|
||
|
||
.status-streaming {
|
||
background: linear-gradient(135deg, #374151, #1f2937);
|
||
color: white;
|
||
}
|
||
|
||
.status-idle {
|
||
background: linear-gradient(135deg, #d1d5db, #9ca3af);
|
||
color: #374151;
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.card h2 {
|
||
color: #4b5563;
|
||
margin-bottom: 20px;
|
||
font-size: 1.5rem;
|
||
border-bottom: 2px solid #6b7280;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: bold;
|
||
color: #374151;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #d1d5db;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #6b7280;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
}
|
||
|
||
.btn {
|
||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 25px;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
transition: all 0.3s;
|
||
margin: 5px;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
background: linear-gradient(135deg, #374151, #1f2937);
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #6b7280, #374151);
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: linear-gradient(135deg, #4b5563, #1f2937);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, #9ca3af, #6b7280);
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||
}
|
||
|
||
.btn-warning {
|
||
background: linear-gradient(135deg, #d1d5db, #9ca3af);
|
||
color: #374151;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: linear-gradient(135deg, #9ca3af, #6b7280);
|
||
color: white;
|
||
}
|
||
|
||
.variables-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.variables-table th,
|
||
.variables-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.variables-table th {
|
||
background: linear-gradient(135deg, #6b7280, #4b5563);
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.variables-table tr:hover {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin: 10px 0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.alert-success {
|
||
background-color: #f3f4f6;
|
||
border: 1px solid #d1d5db;
|
||
color: #374151;
|
||
}
|
||
|
||
.alert-error {
|
||
background-color: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
color: #7f1d1d;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.info-section {
|
||
background-color: #f9fafb;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.info-section p {
|
||
margin: 5px 0;
|
||
color: #374151;
|
||
}
|
||
|
||
.info-section strong {
|
||
color: #1f2937;
|
||
}
|
||
|
||
.log-container {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
background-color: #1f2937;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
border: 1px solid #374151;
|
||
}
|
||
|
||
.log-entry {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 10px;
|
||
padding: 8px;
|
||
border-radius: 5px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.log-entry.log-info {
|
||
background-color: #374151;
|
||
border-left-color: #6b7280;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.log-entry.log-warning {
|
||
background-color: #451a03;
|
||
border-left-color: #f59e0b;
|
||
color: #fef3c7;
|
||
}
|
||
|
||
.log-entry.log-error {
|
||
background-color: #450a0a;
|
||
border-left-color: #ef4444;
|
||
color: #fecaca;
|
||
}
|
||
|
||
.log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.log-timestamp {
|
||
font-size: 10px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.log-type {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.log-message {
|
||
margin-bottom: 4px;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.log-details {
|
||
font-size: 10px;
|
||
opacity: 0.8;
|
||
background-color: rgba(0, 0, 0, 0.2);
|
||
padding: 4px;
|
||
border-radius: 3px;
|
||
margin-top: 4px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.log-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.log-stats {
|
||
background-color: #f9fafb;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 5px;
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
color: #374151;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.header h1 {
|
||
font-size: 2rem;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-logo {
|
||
height: 45px;
|
||
}
|
||
|
||
.form-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>
|
||
<img src="/images/SIDEL.png" alt="SIDEL Logo" class="header-logo">
|
||
🏭 PLC S7-315 Streamer & Logger
|
||
</h1>
|
||
<p>Real-time monitoring and streaming system</p>
|
||
</div>
|
||
|
||
<!-- Status Bar -->
|
||
<div class="status-bar">
|
||
<div class="status-grid">
|
||
<div class="status-item" id="plc-status">
|
||
🔌 PLC: Disconnected
|
||
</div>
|
||
<div class="status-item" id="stream-status">
|
||
📡 Streaming: Inactive
|
||
</div>
|
||
<div class="status-item status-idle">
|
||
📊 Variables: {{ status.variables_count }}
|
||
</div>
|
||
<div class="status-item status-idle">
|
||
⏱️ Interval: {{ status.sampling_interval }}s
|
||
</div>
|
||
<div class="status-item" id="csv-status">
|
||
💾 CSV: Inactive
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status messages -->
|
||
<div id="messages"></div>
|
||
|
||
<!-- PLC Configuration -->
|
||
<div class="card">
|
||
<h2>⚙️ PLC S7-315 Configuration</h2>
|
||
<form id="plc-config-form">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>PLC IP Address:</label>
|
||
<input type="text" id="plc-ip" value="{{ status.plc_config.ip }}" placeholder="192.168.1.100">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Rack:</label>
|
||
<input type="number" id="plc-rack" value="{{ status.plc_config.rack }}" min="0" max="7">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Slot:</label>
|
||
<input type="number" id="plc-slot" value="{{ status.plc_config.slot }}" min="0" max="31">
|
||
</div>
|
||
</div>
|
||
<div class="controls">
|
||
<button type="submit" class="btn">💾 Save Configuration</button>
|
||
<button type="button" class="btn btn-success" id="connect-btn">🔗 Connect PLC</button>
|
||
<button type="button" class="btn btn-danger" id="disconnect-btn">❌ Disconnect PLC</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- UDP Configuration -->
|
||
<div class="card">
|
||
<h2>🌐 UDP Gateway Configuration (PlotJuggler)</h2>
|
||
<form id="udp-config-form">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>UDP Host:</label>
|
||
<input type="text" id="udp-host" value="{{ status.udp_config.host }}" placeholder="127.0.0.1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>UDP Port:</label>
|
||
<input type="number" id="udp-port" value="{{ status.udp_config.port }}" min="1" max="65535">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Sampling Interval (s):</label>
|
||
<input type="number" id="sampling-interval" value="{{ status.sampling_interval }}" min="0.01"
|
||
max="10" step="0.01">
|
||
</div>
|
||
</div>
|
||
<div class="controls">
|
||
<button type="submit" class="btn">💾 Save Configuration</button>
|
||
<button type="button" class="btn btn-warning" id="update-sampling-btn">⏱️ Update Interval</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- PLC Variables -->
|
||
<div class="card">
|
||
<h2>📋 PLC Variables</h2>
|
||
|
||
<!-- Form to add variables -->
|
||
<form id="variable-form">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Variable Name:</label>
|
||
<input type="text" id="var-name" placeholder="temperature" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Data Block (DB):</label>
|
||
<input type="number" id="var-db" min="1" max="9999" value="1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Offset:</label>
|
||
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Data Type:</label>
|
||
<select id="var-type" required>
|
||
<option value="real">REAL (Float 32-bit)</option>
|
||
<option value="int">INT (16-bit)</option>
|
||
<option value="dint">DINT (32-bit)</option>
|
||
<option value="bool">BOOL</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn">➕ Add Variable</button>
|
||
</form>
|
||
|
||
<!-- Variables table -->
|
||
<table class="variables-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Data Block</th>
|
||
<th>Offset</th>
|
||
<th>Type</th>
|
||
<th>Stream to PlotJuggler</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="variables-tbody">
|
||
{% for name, var in variables.items() %}
|
||
<tr>
|
||
<td>{{ name }}</td>
|
||
<td>DB{{ var.db }}</td>
|
||
<td>{{ var.offset }}</td>
|
||
<td>{{ var.type.upper() }}</td>
|
||
<td>
|
||
<input type="checkbox" id="stream-{{ name }}"
|
||
onchange="toggleStreaming('{{ name }}', this.checked)">
|
||
<label for="stream-{{ name }}">Enable</label>
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- CSV Recording Control -->
|
||
<div class="card">
|
||
<h2>💾 CSV Recording Control</h2>
|
||
<div class="info-section">
|
||
<p><strong>📁 Recording Location:</strong> records/[dd-mm-yyyy]/[hour].csv</p>
|
||
<p><strong>📊 Recording Mode:</strong> All defined variables are automatically saved to CSV</p>
|
||
<p><strong>📅 File Organization:</strong> One file per hour, automatic directory creation</p>
|
||
<p id="current-csv-file"></p>
|
||
</div>
|
||
<div class="controls">
|
||
<button class="btn btn-success" id="start-csv-btn">💾 Start CSV Recording</button>
|
||
<button class="btn btn-danger" id="stop-csv-btn">⏹️ Stop CSV Recording</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Streaming Control -->
|
||
<div class="card">
|
||
<h2>🚀 Streaming Control</h2>
|
||
<div class="info-section">
|
||
<p><strong>📡 Streaming Mode:</strong> Only variables marked for streaming are sent to PlotJuggler</p>
|
||
<p><strong>🔄 Combined Operation:</strong> Starting streaming also starts CSV recording automatically
|
||
</p>
|
||
</div>
|
||
<div class="controls">
|
||
<button class="btn btn-success" id="start-streaming-btn">▶️ Start Streaming & CSV</button>
|
||
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Stop Streaming & CSV</button>
|
||
<button class="btn" onclick="location.reload()">🔄 Refresh Status</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Application Events Log -->
|
||
<div class="card">
|
||
<h2>📋 Application Events Log</h2>
|
||
<div class="info-section">
|
||
<p><strong>📝 Event Tracking:</strong> Connection events, configuration changes, errors and system
|
||
status</p>
|
||
<p><strong>💾 Persistent Storage:</strong> Events are saved to disk and persist between application
|
||
restarts</p>
|
||
</div>
|
||
|
||
<div class="log-controls">
|
||
<button class="btn" onclick="refreshEventLog()">🔄 Refresh Log</button>
|
||
<button class="btn" onclick="clearLogView()">🧹 Clear View</button>
|
||
<select id="log-limit" onchange="refreshEventLog()">
|
||
<option value="25">Last 25 events</option>
|
||
<option value="50" selected>Last 50 events</option>
|
||
<option value="100">Last 100 events</option>
|
||
<option value="200">Last 200 events</option>
|
||
</select>
|
||
<div class="log-stats" id="log-stats">
|
||
Loading log statistics...
|
||
</div>
|
||
</div>
|
||
|
||
<div class="log-container" id="events-log">
|
||
<div class="log-entry log-info">
|
||
<div class="log-header">
|
||
<span>📡 System</span>
|
||
<span class="log-timestamp">Loading...</span>
|
||
</div>
|
||
<div class="log-message">Loading application events...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Function to display messages
|
||
function showMessage(message, type = 'success') {
|
||
const messagesDiv = document.getElementById('messages');
|
||
const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
|
||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||
setTimeout(() => {
|
||
messagesDiv.innerHTML = '';
|
||
}, 5000);
|
||
}
|
||
|
||
// Function to update visual status
|
||
function updateStatus() {
|
||
fetch('/api/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const plcStatus = document.getElementById('plc-status');
|
||
const streamStatus = document.getElementById('stream-status');
|
||
const csvStatus = document.getElementById('csv-status');
|
||
|
||
if (data.plc_connected) {
|
||
plcStatus.textContent = '🔌 PLC: Connected';
|
||
plcStatus.className = 'status-item status-connected';
|
||
} else {
|
||
plcStatus.textContent = '🔌 PLC: Disconnected';
|
||
plcStatus.className = 'status-item status-disconnected';
|
||
}
|
||
|
||
if (data.streaming) {
|
||
streamStatus.textContent = `📡 Streaming: Active (${data.streaming_variables_count} vars)`;
|
||
streamStatus.className = 'status-item status-streaming';
|
||
} else {
|
||
streamStatus.textContent = '📡 Streaming: Inactive';
|
||
streamStatus.className = 'status-item status-idle';
|
||
}
|
||
|
||
if (data.csv_recording) {
|
||
csvStatus.textContent = '💾 CSV: Recording';
|
||
csvStatus.className = 'status-item status-streaming';
|
||
} else {
|
||
csvStatus.textContent = '💾 CSV: Inactive';
|
||
csvStatus.className = 'status-item status-idle';
|
||
}
|
||
|
||
// Update current CSV file info
|
||
const csvFileInfo = document.getElementById('current-csv-file');
|
||
if (data.current_csv_file && data.csv_recording) {
|
||
csvFileInfo.innerHTML = `<strong>📁 Current File:</strong> ${data.current_csv_file}`;
|
||
} else {
|
||
csvFileInfo.innerHTML = '';
|
||
}
|
||
})
|
||
.catch(error => console.error('Error updating status:', error));
|
||
}
|
||
|
||
// PLC Configuration
|
||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
const data = {
|
||
ip: document.getElementById('plc-ip').value,
|
||
rack: parseInt(document.getElementById('plc-rack').value),
|
||
slot: parseInt(document.getElementById('plc-slot').value)
|
||
};
|
||
|
||
fetch('/api/plc/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
});
|
||
});
|
||
|
||
// UDP Configuration
|
||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
const data = {
|
||
host: document.getElementById('udp-host').value,
|
||
port: parseInt(document.getElementById('udp-port').value)
|
||
};
|
||
|
||
fetch('/api/udp/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
});
|
||
});
|
||
|
||
// Connect PLC
|
||
document.getElementById('connect-btn').addEventListener('click', function () {
|
||
fetch('/api/plc/connect', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Disconnect PLC
|
||
document.getElementById('disconnect-btn').addEventListener('click', function () {
|
||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Add variable
|
||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
const data = {
|
||
name: document.getElementById('var-name').value,
|
||
db: parseInt(document.getElementById('var-db').value),
|
||
offset: parseInt(document.getElementById('var-offset').value),
|
||
type: document.getElementById('var-type').value
|
||
};
|
||
|
||
fetch('/api/variables', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
if (data.success) {
|
||
location.reload();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Toggle streaming for variable
|
||
function toggleStreaming(varName, enabled) {
|
||
fetch(`/api/variables/${varName}/streaming`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled: enabled })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus(); // Update streaming variable count
|
||
})
|
||
.catch(error => {
|
||
console.error('Error toggling streaming:', error);
|
||
showMessage('Error updating streaming setting', 'error');
|
||
});
|
||
}
|
||
|
||
// Load streaming variables status
|
||
function loadStreamingStatus() {
|
||
fetch('/api/variables/streaming')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
data.streaming_variables.forEach(varName => {
|
||
const checkbox = document.getElementById(`stream-${varName}`);
|
||
if (checkbox) {
|
||
checkbox.checked = true;
|
||
}
|
||
});
|
||
}
|
||
})
|
||
.catch(error => console.error('Error loading streaming status:', error));
|
||
}
|
||
|
||
// Remove variable
|
||
function removeVariable(name) {
|
||
if (confirm(`Are you sure you want to remove the variable "${name}"?`)) {
|
||
fetch(`/api/variables/${name}`, { method: 'DELETE' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
if (data.success) {
|
||
location.reload();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Start streaming
|
||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||
fetch('/api/streaming/start', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Stop streaming
|
||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||
fetch('/api/streaming/stop', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Update interval
|
||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||
fetch('/api/sampling', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ interval: interval })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
});
|
||
});
|
||
|
||
// Start CSV recording
|
||
document.getElementById('start-csv-btn').addEventListener('click', function () {
|
||
fetch('/api/csv/start', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Stop CSV recording
|
||
document.getElementById('stop-csv-btn').addEventListener('click', function () {
|
||
fetch('/api/csv/stop', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
|
||
// Application Events Log Functions
|
||
function formatTimestamp(isoString) {
|
||
const date = new Date(isoString);
|
||
return date.toLocaleString('es-ES', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
}
|
||
|
||
function getEventIcon(eventType) {
|
||
const icons = {
|
||
'plc_connection': '🔗',
|
||
'plc_connection_failed': '❌',
|
||
'plc_disconnection': '🔌',
|
||
'plc_disconnection_error': '⚠️',
|
||
'streaming_started': '▶️',
|
||
'streaming_stopped': '⏹️',
|
||
'streaming_error': '❌',
|
||
'csv_started': '💾',
|
||
'csv_stopped': '📁',
|
||
'csv_error': '❌',
|
||
'config_change': '⚙️',
|
||
'variable_added': '➕',
|
||
'variable_removed': '➖',
|
||
'application_started': '🚀'
|
||
};
|
||
return icons[eventType] || '📋';
|
||
}
|
||
|
||
function createLogEntry(event) {
|
||
const logEntry = document.createElement('div');
|
||
logEntry.className = `log-entry log-${event.level}`;
|
||
|
||
const hasDetails = event.details && Object.keys(event.details).length > 0;
|
||
|
||
logEntry.innerHTML = `
|
||
<div class="log-header">
|
||
<span>${getEventIcon(event.event_type)} ${event.event_type.replace(/_/g, ' ').toUpperCase()}</span>
|
||
<span class="log-timestamp">${formatTimestamp(event.timestamp)}</span>
|
||
</div>
|
||
<div class="log-message">${event.message}</div>
|
||
${hasDetails ? `<div class="log-details">${JSON.stringify(event.details, null, 2)}</div>` : ''}
|
||
`;
|
||
|
||
return logEntry;
|
||
}
|
||
|
||
function refreshEventLog() {
|
||
const limit = document.getElementById('log-limit').value;
|
||
|
||
fetch(`/api/events?limit=${limit}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const logContainer = document.getElementById('events-log');
|
||
const logStats = document.getElementById('log-stats');
|
||
|
||
// Clear existing entries
|
||
logContainer.innerHTML = '';
|
||
|
||
// Update statistics
|
||
logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`;
|
||
|
||
// Add events (reverse order to show newest first)
|
||
const events = data.events.reverse();
|
||
|
||
if (events.length === 0) {
|
||
logContainer.innerHTML = `
|
||
<div class="log-entry log-info">
|
||
<div class="log-header">
|
||
<span>📋 System</span>
|
||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||
</div>
|
||
<div class="log-message">No events found</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
events.forEach(event => {
|
||
logContainer.appendChild(createLogEntry(event));
|
||
});
|
||
}
|
||
|
||
// Auto-scroll to top to show newest events
|
||
logContainer.scrollTop = 0;
|
||
} else {
|
||
console.error('Error loading events:', data.error);
|
||
showMessage('Error loading events log', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching events:', error);
|
||
showMessage('Error fetching events log', 'error');
|
||
});
|
||
}
|
||
|
||
function clearLogView() {
|
||
const logContainer = document.getElementById('events-log');
|
||
logContainer.innerHTML = `
|
||
<div class="log-entry log-info">
|
||
<div class="log-header">
|
||
<span>🧹 System</span>
|
||
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
|
||
</div>
|
||
<div class="log-message">Log view cleared. Click refresh to reload events.</div>
|
||
</div>
|
||
`;
|
||
|
||
const logStats = document.getElementById('log-stats');
|
||
logStats.textContent = 'Log view cleared';
|
||
}
|
||
|
||
// Update status every 5 seconds
|
||
setInterval(updateStatus, 5000);
|
||
|
||
// Update event log every 10 seconds
|
||
setInterval(refreshEventLog, 10000);
|
||
|
||
// Initial update
|
||
updateStatus();
|
||
loadStreamingStatus();
|
||
refreshEventLog();
|
||
</script>
|
||
</body>
|
||
|
||
</html> |