S7_snap7_Stremer_n_Recorder/templates/index.html

617 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>PLC S7-315 Streamer & Logger</title>
<!-- Pico.css offline -->
<link rel="stylesheet" href="/static/css/pico.min.css">
<!-- Custom styles -->
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<!-- Theme Selector -->
<div class="theme-selector">
<button id="theme-light" class="active" onclick="setTheme('light')">☀️ Light</button>
<button id="theme-dark" onclick="setTheme('dark')">🌙 Dark</button>
<button id="theme-auto" onclick="setTheme('auto')">🔄 Auto</button>
</div>
<main class="container">
<!-- Header -->
<header class="header">
<h1>
<img src="/images/SIDEL.png" alt="SIDEL Logo" class="header-logo">
-- PLC S7-31x Streamer & Logger
</h1>
<p>Real-time monitoring and streaming system</p>
</header>
<!-- Status Bar -->
<section class="status-grid">
<div class="status-item" id="plc-status">
🔌 PLC: Disconnected
<div style="margin-top: 8px;">
<button type="button" id="status-connect-btn">🔗 Connect</button>
</div>
</div>
<div class="status-item" id="stream-status">
📡 Streaming: Inactive
<div style="margin-top: 8px;">
<button type="button" id="status-streaming-btn">⏹️ Stop</button>
</div>
</div>
<div class="status-item status-idle">
📊 Datasets: {{ status.datasets_count }} ({{ status.active_datasets_count }} active)
</div>
<div class="status-item status-idle">
📋 Variables: {{ status.total_variables }}
</div>
<div class="status-item status-idle">
⏱️ Interval: {{ status.sampling_interval }}s
</div>
<div class="status-item" id="csv-status">
💾 CSV: Inactive
</div>
<div class="status-item" id="disk-space">
💽 Disk Space: Calculating...
</div>
</section>
<!-- Status messages -->
<div id="messages"></div>
<!-- Configuration Grid -->
<div class="config-grid">
<!-- PLC Configuration -->
<article>
<header>⚙️ PLC S7-315 Configuration</header>
<form id="plc-config-form">
<div class="form-row">
<label>
PLC IP Address:
<input type="text" id="plc-ip" value="{{ status.plc_config.ip }}"
placeholder="192.168.1.100">
</label>
<label>
Rack:
<input type="number" id="plc-rack" value="{{ status.plc_config.rack }}" min="0" max="7">
</label>
<label>
Slot:
<input type="number" id="plc-slot" value="{{ status.plc_config.slot }}" min="0" max="31">
</label>
</div>
<div class="controls">
<button type="submit">💾 Save Configuration</button>
<button type="button" id="connect-btn">🔗 Connect PLC</button>
<button type="button" id="disconnect-btn" class="secondary">❌ Disconnect PLC</button>
</div>
</form>
</article>
<!-- UDP Configuration -->
<article>
<header>🌐 UDP Gateway Configuration (PlotJuggler)</header>
<form id="udp-config-form">
<div class="form-row">
<label>
UDP Host:
<input type="text" id="udp-host" value="{{ status.udp_config.host }}"
placeholder="127.0.0.1">
</label>
<label>
UDP Port:
<input type="number" id="udp-port" value="{{ status.udp_config.port }}" min="1" max="65535">
</label>
<label>
Sampling Interval (s):
<input type="number" id="sampling-interval" value="{{ status.sampling_interval }}"
min="0.01" max="10" step="0.01">
</label>
</div>
<div class="controls">
<button type="submit">💾 Save Configuration</button>
<button type="button" id="update-sampling-btn" class="secondary">⏱️ Update Interval</button>
</div>
</form>
</article>
</div>
<!-- Integrated Dataset & Variables Management -->
<article>
<header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>📊 Dataset & Variables Management</span>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<select id="dataset-selector" style="min-width: 200px;">
<option value="">Select a dataset...</option>
</select>
<button type="button" id="new-dataset-btn" class="outline"> New</button>
<button type="button" id="delete-dataset-btn" class="secondary">🗑️ Delete</button>
</div>
</div>
</header>
<!-- Dataset Status Bar -->
<div id="dataset-status-bar" style="display: none; margin-bottom: 1rem;">
<div class="info-section" style="margin-bottom: 0;">
<div
style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
<span><strong>📂 Name:</strong> <span id="dataset-name"></span></span>
<span><strong>🏷️ Prefix:</strong> <span id="dataset-prefix"></span></span>
<span><strong>⏱️ Sampling:</strong> <span id="dataset-sampling"></span></span>
<span><strong>📊 Variables:</strong> <span id="dataset-var-count"></span></span>
<span><strong>📡 Streaming:</strong> <span id="dataset-stream-count"></span></span>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<span id="dataset-status" class="status-item"></span>
<button type="button" id="activate-dataset-btn" class="outline">▶️ Activate</button>
<button type="button" id="deactivate-dataset-btn" class="secondary">⏹️ Deactivate</button>
</div>
</div>
</div>
</div>
<!-- Variables Management Section -->
<div id="variables-management" style="display: none;">
<!-- Real-time Streaming Control -->
<div
style="margin-bottom: 1rem; padding: 1rem; background: var(--pico-card-background-color); border-radius: var(--pico-border-radius); border: var(--pico-border-width) solid var(--pico-border-color);">
<div
style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<strong>🔄 Real-time Variable Streaming</strong>
<br>
<small style="color: var(--pico-muted-color);">
Enable live updates of variable values without page refresh
</small>
</div>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button type="button" id="toggle-streaming-btn" class="outline"
onclick="toggleRealTimeStreaming()">
▶️ Start Live Streaming
</button>
<button type="button" id="refresh-values-btn" onclick="refreshVariableValues()">
🔄 Refresh Values
</button>
</div>
</div>
<div id="last-refresh-time"
style="margin-top: 0.5rem; font-size: 0.9em; color: var(--pico-muted-color);">
Click "Refresh Values" to read current variable values
</div>
</div>
<!-- Add Variable Form -->
<form id="variable-form" style="margin-bottom: 1.5rem;">
<div class="form-row">
<label>
Variable Name:
<input type="text" id="var-name" placeholder="temperature" required>
</label>
<label>
Memory Area:
<select id="var-area" required onchange="toggleFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</label>
<label id="db-field">
Data Block (DB):
<input type="number" id="var-db" min="1" max="9999" value="1" required>
</label>
<label>
Offset:
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
</label>
<label id="bit-field" style="display: none;">
Bit Position:
<select id="var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</label>
<label>
Data Type:
<select id="var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</option>
<option value="bool">BOOL</option>
</select>
</label>
</div>
<button type="submit"> Add Variable</button>
</form>
<!-- Variables Table -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h4 style="margin: 0;">📊 Variables in Dataset</h4>
<div style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
<button type="button" id="refresh-values-btn" class="outline" onclick="refreshVariableValues()">
🔄 Refresh Values
</button>
<button type="button" id="diagnose-btn" class="secondary" onclick="diagnoseConnection()"
title="Run connection and variable diagnostics">
🔍 Diagnose
</button>
<div style="margin-left: 1rem;">
<span id="last-refresh-time"
style="color: var(--pico-muted-color); font-size: 0.9rem;"></span>
</div>
</div>
</div>
<table class="variables-table">
<thead>
<tr>
<th>Name</th>
<th>Memory Area</th>
<th>Offset</th>
<th>Type</th>
<th>Current Value</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>
{% if var.area == 'db' or var.get('db') %}
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
{% elif var.area == 'mw' or var.area == 'm' %}
MW{{ var.offset }}
{% elif var.area == 'pew' or var.area == 'pe' %}
PEW{{ var.offset }}
{% elif var.area == 'paw' or var.area == 'pa' %}
PAW{{ var.offset }}
{% elif var.area == 'e' %}
E{{ var.offset }}.{{ var.bit }}
{% elif var.area == 'a' %}
A{{ var.offset }}.{{ var.bit }}
{% elif var.area == 'mb' %}
M{{ var.offset }}.{{ var.bit }}
{% else %}
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
{% endif %}
</td>
<td>{{ var.offset }}</td>
<td>{{ var.type.upper() }}</td>
<td id="value-{{ name }}"
style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
--
</td>
<td>
<label>
<input type="checkbox" id="stream-{{ name }}" role="switch"
onchange="toggleStreaming('{{ name }}', this.checked)">
Enable
</label>
</td>
<td>
<button class="outline" onclick="editVariable('{{ name }}')">✏️ Edit</button>
<button class="secondary" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- No Dataset Selected Message -->
<div id="no-dataset-message" style="text-align: center; padding: 2rem; color: var(--pico-muted-color);">
<p>📊 Please select a dataset to manage its variables</p>
<p>Or create a new dataset to get started</p>
</div>
</article>
<!-- Multi-Dataset Streaming Control -->
<article>
<header>🚀 Multi-Dataset Streaming Control</header>
<div class="info-section">
<p><strong>📡 Streaming Mode:</strong> Only variables marked for streaming in active datasets are sent
to PlotJuggler</p>
<p><strong>💾 CSV Recording:</strong> Each active dataset automatically records ALL its variables to
separate CSV files</p>
<p><strong>📁 File Organization:</strong> records/[dd-mm-yyyy]/[prefix]_[hour].csv (e.g., temp_14.csv,
pressure_14.csv)</p>
<p><strong>⏱️ Individual Sampling:</strong> Each dataset can have its own sampling interval or use the
global one</p>
</div>
<div class="controls">
<button id="start-streaming-btn">▶️ Start All Active Datasets</button>
<button class="secondary" id="stop-streaming-btn">⏹️ Stop All Streaming</button>
<button class="outline" onclick="location.reload()">🔄 Refresh Status</button>
</div>
</article>
<!-- CSV Recording Configuration -->
<article>
<header>📁 CSV Recording Configuration</header>
<div class="info-section">
<p><strong>📂 Directory Management:</strong> Configure where CSV files are saved and manage file
rotation</p>
<p><strong>🔄 Automatic Cleanup:</strong> Set limits by size, days, or hours to automatically delete old
files</p>
<p><strong>💿 Disk Space:</strong> Monitor available space and estimated recording time remaining</p>
</div>
<!-- Current Configuration Display -->
<div class="csv-config-display" id="csv-config-display">
<div class="config-grid">
<div class="config-item">
<strong>📁 Records Directory:</strong>
<span id="csv-directory-path">Loading...</span>
</div>
<div class="config-item">
<strong>🔄 Rotation Enabled:</strong>
<span id="csv-rotation-enabled">Loading...</span>
</div>
<div class="config-item">
<strong>📊 Max Size:</strong>
<span id="csv-max-size">Loading...</span>
</div>
<div class="config-item">
<strong>📅 Max Days:</strong>
<span id="csv-max-days">Loading...</span>
</div>
<div class="config-item">
<strong>⏰ Max Hours:</strong>
<span id="csv-max-hours">Loading...</span>
</div>
<div class="config-item">
<strong>🧹 Cleanup Interval:</strong>
<span id="csv-cleanup-interval">Loading...</span>
</div>
</div>
</div>
<!-- Directory Information -->
<div class="csv-directory-info" id="csv-directory-info" style="margin-top: 1rem;">
<details>
<summary><strong>📊 Directory Information</strong></summary>
<div class="directory-stats" id="directory-stats">
<p>Loading directory information...</p>
</div>
</details>
</div>
<!-- Configuration Form -->
<details style="margin-top: 1rem;">
<summary><strong>⚙️ Modify Configuration</strong></summary>
<form id="csv-config-form" style="margin-top: 1rem;">
<div class="grid">
<div>
<label for="records-directory">📁 Records Directory:</label>
<input type="text" id="records-directory" name="records_directory" placeholder="records"
required>
<small>Base directory where CSV files will be saved</small>
</div>
<div>
<label>
<input type="checkbox" id="rotation-enabled" name="rotation_enabled">
🔄 Enable File Rotation
</label>
<small>Automatically delete old files based on limits below</small>
</div>
</div>
<div class="grid">
<div>
<label for="max-size-mb">📊 Max Total Size (MB):</label>
<input type="number" id="max-size-mb" name="max_size_mb" min="1" step="1"
placeholder="1000">
<small>Maximum total size of all CSV files in MB (leave empty for no limit)</small>
</div>
<div>
<label for="max-days">📅 Max Days to Keep:</label>
<input type="number" id="max-days" name="max_days" min="1" step="1" placeholder="30">
<small>Delete files older than this many days (leave empty for no limit)</small>
</div>
</div>
<div class="grid">
<div>
<label for="max-hours">⏰ Max Hours to Keep:</label>
<input type="number" id="max-hours" name="max_hours" min="1" step="1" placeholder="">
<small>Delete files older than this many hours (overrides days setting)</small>
</div>
<div>
<label for="cleanup-interval">🧹 Cleanup Interval (hours):</label>
<input type="number" id="cleanup-interval" name="cleanup_interval_hours" min="1" step="1"
value="24" required>
<small>How often to run automatic cleanup</small>
</div>
</div>
<div class="controls">
<button type="submit">💾 Save Configuration</button>
<button type="button" class="secondary" onclick="loadCsvConfig()">🔄 Reload</button>
<button type="button" class="outline" onclick="triggerManualCleanup()">🧹 Manual
Cleanup</button>
</div>
</form>
</details>
</article>
<!-- Application Events Log -->
<article>
<header>📋 Application Events Log</header>
<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="outline" onclick="refreshEventLog()">🔄 Refresh Log</button>
<button class="outline" 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>
</article>
</main>
<!-- Edit Variable Modal -->
<div id="edit-modal" class="modal" style="display: none;">
<article>
<header>
<h3>✏️ Edit Variable</h3>
<button class="close" onclick="closeEditModal()" aria-label="Close">&times;</button>
</header>
<form id="edit-variable-form">
<div class="form-row">
<label>
Variable Name:
<input type="text" id="edit-var-name" required>
</label>
<label>
Memory Area:
<select id="edit-var-area" required onchange="toggleEditFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</label>
<label id="edit-db-field">
Data Block (DB):
<input type="number" id="edit-var-db" min="1" max="9999" value="1" required>
</label>
<label>
Offset:
<input type="number" id="edit-var-offset" min="0" max="8192" value="0" required>
</label>
<label id="edit-bit-field" style="display: none;">
Bit Position:
<select id="edit-var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</label>
<label>
Data Type:
<select id="edit-var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</option>
<option value="bool">BOOL (Boolean)</option>
</select>
</label>
</div>
<footer>
<button type="button" class="secondary" onclick="closeEditModal()">❌ Cancel</button>
<button type="submit">💾 Update Variable</button>
</footer>
</form>
</article>
</div>
<!-- New Dataset Modal -->
<div id="dataset-modal" class="modal" style="display: none;">
<article>
<header>
<h3>Create New Dataset</h3>
<button class="close" id="close-dataset-modal" aria-label="Close">&times;</button>
</header>
<form id="dataset-form">
<label>
Dataset ID:
<input type="text" id="dataset-id" placeholder="e.g., temperature_sensors" required>
<small>Used internally, no spaces or special characters</small>
</label>
<label>
Dataset Name:
<input type="text" id="dataset-name-input" placeholder="e.g., Temperature Sensors" required>
</label>
<label>
CSV Prefix:
<input type="text" id="dataset-prefix-input" placeholder="e.g., temp" required>
<small>Files will be named: prefix_hour.csv (e.g., temp_14.csv)</small>
</label>
<label>
Sampling Interval (seconds):
<input type="number" id="dataset-sampling-input" placeholder="Leave empty to use global interval"
min="0.01" step="0.01">
<small>Leave empty to use global sampling interval</small>
</label>
<footer>
<button type="button" class="secondary" id="cancel-dataset-btn">Cancel</button>
<button type="submit">Create Dataset</button>
</footer>
</form>
</article>
</div>
<!-- JavaScript Modules -->
<script src="/static/js/utils.js"></script>
<script src="/static/js/theme.js"></script>
<script src="/static/js/status.js"></script>
<script src="/static/js/plc.js"></script>
<script src="/static/js/variables.js"></script>
<script src="/static/js/datasets.js"></script>
<script src="/static/js/streaming.js"></script>
<script src="/static/js/csv.js"></script>
<script src="/static/js/events.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>