1809 lines
73 KiB
HTML
1809 lines
73 KiB
HTML
<!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 for specific components -->
|
||
<style>
|
||
/* Header with logo */
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.header h1 {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-logo {
|
||
height: 1.2em;
|
||
width: auto;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* Status grid */
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.status-item {
|
||
padding: 1rem;
|
||
border-radius: var(--pico-border-radius);
|
||
text-align: center;
|
||
font-weight: bold;
|
||
background: var(--pico-card-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||
}
|
||
|
||
.status-connected {
|
||
background: var(--pico-primary-background);
|
||
color: var(--pico-primary-inverse);
|
||
}
|
||
|
||
.status-disconnected {
|
||
background: var(--pico-secondary-background);
|
||
color: var(--pico-secondary-inverse);
|
||
}
|
||
|
||
.status-streaming {
|
||
background: var(--pico-primary-background);
|
||
color: var(--pico-primary-inverse);
|
||
}
|
||
|
||
.status-idle {
|
||
background: var(--pico-muted-background-color);
|
||
color: var(--pico-muted-color);
|
||
}
|
||
|
||
/* Configuration grid */
|
||
.config-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* Form styling */
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
/* Variables table */
|
||
.variables-table {
|
||
width: 100%;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
/* Alert styles */
|
||
.alert {
|
||
padding: 1rem;
|
||
border-radius: var(--pico-border-radius);
|
||
margin: 1rem 0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.alert-success {
|
||
background-color: var(--pico-primary-background);
|
||
color: var(--pico-primary-inverse);
|
||
}
|
||
|
||
.alert-error {
|
||
background-color: var(--pico-color-red-400);
|
||
color: white;
|
||
}
|
||
|
||
/* Dataset controls */
|
||
.dataset-controls {
|
||
background: var(--pico-card-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||
padding: 1.5rem;
|
||
border-radius: var(--pico-border-radius);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* Info section */
|
||
.info-section {
|
||
background: var(--pico-muted-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-muted-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.info-section p {
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
/* Log container */
|
||
.log-container {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
background: var(--pico-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 1rem;
|
||
font-family: var(--pico-font-family-monospace);
|
||
}
|
||
|
||
.log-entry {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 1rem;
|
||
padding: 0.75rem;
|
||
border-radius: var(--pico-border-radius);
|
||
font-size: 0.875rem;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.log-entry.log-info {
|
||
background: var(--pico-card-background-color);
|
||
border-left-color: var(--pico-primary);
|
||
}
|
||
|
||
.log-entry.log-warning {
|
||
background: var(--pico-color-amber-50);
|
||
border-left-color: var(--pico-color-amber-500);
|
||
}
|
||
|
||
.log-entry.log-error {
|
||
background: var(--pico-color-red-50);
|
||
border-left-color: var(--pico-color-red-500);
|
||
}
|
||
|
||
.log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: bold;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.log-timestamp {
|
||
font-size: 0.75rem;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.log-message {
|
||
margin-bottom: 0.25rem;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.log-details {
|
||
font-size: 0.75rem;
|
||
opacity: 0.8;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
padding: 0.5rem;
|
||
border-radius: var(--pico-border-radius);
|
||
margin-top: 0.25rem;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.log-controls {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.log-stats {
|
||
background: var(--pico-muted-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-muted-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* Utility classes */
|
||
.status-active {
|
||
color: var(--pico-primary);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status-inactive {
|
||
color: var(--pico-muted-color);
|
||
}
|
||
|
||
.status-error {
|
||
color: var(--pico-color-red-500);
|
||
}
|
||
|
||
/* Modal improvements */
|
||
.modal {
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.modal article {
|
||
margin: 5% auto;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.modal header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.close {
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
border: none;
|
||
background: none;
|
||
color: inherit;
|
||
}
|
||
|
||
.close:hover {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* Mobile responsive */
|
||
@media (max-width: 768px) {
|
||
.header h1 {
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.header-logo {
|
||
height: 1.2em;
|
||
}
|
||
|
||
.config-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.form-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.status-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Theme selector */
|
||
.theme-selector {
|
||
position: fixed;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
z-index: 1000;
|
||
background: var(--pico-card-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 0.5rem;
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
box-shadow: var(--pico-box-shadow);
|
||
}
|
||
|
||
.theme-selector button {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.75rem;
|
||
border-radius: var(--pico-border-radius);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.theme-selector button.active {
|
||
background: var(--pico-primary-background);
|
||
color: var(--pico-primary-inverse);
|
||
}
|
||
|
||
.theme-selector button:not(.active) {
|
||
background: var(--pico-muted-background-color);
|
||
color: var(--pico-muted-color);
|
||
}
|
||
|
||
.theme-selector button:hover:not(.active) {
|
||
background: var(--pico-primary-hover);
|
||
color: var(--pico-primary-inverse);
|
||
}
|
||
|
||
/* Font size reduction - 15% smaller (more balanced) */
|
||
html {
|
||
font-size: 85%;
|
||
/* 15% reduction from 100% - more balanced */
|
||
}
|
||
|
||
/* Adjust specific elements that might need fine-tuning */
|
||
.header h1 {
|
||
font-size: 2.2rem;
|
||
/* Adjusted for smaller base font */
|
||
}
|
||
|
||
.header p {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.status-item {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.log-entry {
|
||
font-size: 0.9rem;
|
||
/* Adjusted for smaller base font */
|
||
}
|
||
|
||
.log-timestamp {
|
||
font-size: 0.8rem;
|
||
/* Adjusted for smaller base font */
|
||
}
|
||
|
||
.log-details {
|
||
font-size: 0.8rem;
|
||
/* Adjusted for smaller base font */
|
||
}
|
||
|
||
.log-stats {
|
||
font-size: 0.9rem;
|
||
/* Adjusted for smaller base font */
|
||
}
|
||
|
||
/* Ensure buttons and inputs remain readable */
|
||
button,
|
||
input,
|
||
select,
|
||
textarea {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* Table adjustments */
|
||
.variables-table th,
|
||
.variables-table td {
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
/* Modal adjustments */
|
||
.modal article {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.modal h3 {
|
||
font-size: 1.4rem;
|
||
}
|
||
</style>
|
||
</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>
|
||
|
||
<!-- Dataset Management -->
|
||
<article>
|
||
<header>📊 Dataset Management</header>
|
||
|
||
<!-- Dataset Selector and Controls -->
|
||
<div class="dataset-controls">
|
||
<div class="form-row">
|
||
<label>
|
||
Current Dataset:
|
||
<select id="dataset-selector">
|
||
<option value="">Select a dataset...</option>
|
||
</select>
|
||
</label>
|
||
<div class="controls">
|
||
<button type="button" id="new-dataset-btn">➕ New Dataset</button>
|
||
<button type="button" id="delete-dataset-btn" class="secondary">🗑️ Delete Dataset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row" id="dataset-actions" style="display: none;">
|
||
<div class="controls">
|
||
<button type="button" id="activate-dataset-btn">▶️ Activate</button>
|
||
<button type="button" id="deactivate-dataset-btn" class="secondary">⏹️ Deactivate</button>
|
||
</div>
|
||
<div>
|
||
<span id="dataset-status" class="status-item"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dataset Information -->
|
||
<div id="dataset-info" style="display: none;">
|
||
<div class="info-section">
|
||
<p><strong>📂 Name:</strong> <span id="dataset-name"></span></p>
|
||
<p><strong>🏷️ Prefix:</strong> <span id="dataset-prefix"></span></p>
|
||
<p><strong>⏱️ Sampling:</strong> <span id="dataset-sampling"></span></p>
|
||
<p><strong>📊 Variables:</strong> <span id="dataset-var-count"></span> | <strong>📡
|
||
Streaming:</strong> <span id="dataset-stream-count"></span></p>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- 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">×</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>
|
||
|
||
<!-- PLC Variables for Current Dataset -->
|
||
<article id="variables-section" style="display: none;">
|
||
<header>📋 Variables for: <span id="current-dataset-title"></span></header>
|
||
|
||
<!-- Form to add variables to current dataset -->
|
||
<form id="variable-form">
|
||
<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 -->
|
||
<table class="variables-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Memory Area</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>
|
||
{% 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>
|
||
<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>
|
||
</article>
|
||
|
||
<!-- 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">×</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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<script>
|
||
// Theme management
|
||
function setTheme(theme) {
|
||
const html = document.documentElement;
|
||
const buttons = document.querySelectorAll('.theme-selector button');
|
||
|
||
// Remove active class from all buttons
|
||
buttons.forEach(btn => btn.classList.remove('active'));
|
||
|
||
// Set theme
|
||
html.setAttribute('data-theme', theme);
|
||
|
||
// Add active class to selected button
|
||
document.getElementById(`theme-${theme}`).classList.add('active');
|
||
|
||
// Save preference to localStorage
|
||
localStorage.setItem('theme', theme);
|
||
}
|
||
|
||
// Load saved theme on page load
|
||
function loadTheme() {
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
setTheme(savedTheme);
|
||
}
|
||
|
||
// Initialize theme on page load
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
loadTheme();
|
||
});
|
||
|
||
// 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);
|
||
}
|
||
|
||
// Update status display
|
||
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');
|
||
const diskSpaceStatus = document.getElementById('disk-space');
|
||
|
||
// Update PLC connection status
|
||
if (data.plc_connected) {
|
||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||
plcStatus.className = 'status-item status-connected';
|
||
|
||
// Add event listener to the new disconnect button
|
||
document.getElementById('status-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();
|
||
});
|
||
});
|
||
} else {
|
||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||
plcStatus.className = 'status-item status-disconnected';
|
||
|
||
// Add event listener to the connect button
|
||
document.getElementById('status-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();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Update streaming status
|
||
if (data.streaming) {
|
||
streamStatus.innerHTML = '📡 Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||
streamStatus.className = 'status-item status-streaming';
|
||
|
||
// Add event listener to the stop streaming button
|
||
document.getElementById('status-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();
|
||
});
|
||
});
|
||
} else {
|
||
streamStatus.innerHTML = '📡 Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||
streamStatus.className = 'status-item status-idle';
|
||
|
||
// Add event listener to the start streaming button
|
||
document.getElementById('status-start-btn').addEventListener('click', function () {
|
||
fetch('/api/streaming/start', { method: 'POST' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
updateStatus();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Update CSV recording status
|
||
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 disk space status
|
||
if (data.disk_space_info) {
|
||
diskSpaceStatus.innerHTML = `💽 Disk: ${data.disk_space_info.free_space} free<br>
|
||
⏱️ ~${data.disk_space_info.recording_time_left}`;
|
||
diskSpaceStatus.className = 'status-item status-idle';
|
||
} else {
|
||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||
diskSpaceStatus.className = 'status-item status-idle';
|
||
}
|
||
})
|
||
.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();
|
||
});
|
||
});
|
||
|
||
// Status grid connect button
|
||
document.getElementById('status-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();
|
||
});
|
||
});
|
||
|
||
// Toggle DB and Bit field visibility based on memory area selection
|
||
function toggleFields() {
|
||
const area = document.getElementById('var-area').value;
|
||
const dbField = document.getElementById('db-field');
|
||
const dbInput = document.getElementById('var-db');
|
||
const bitField = document.getElementById('bit-field');
|
||
const typeSelect = document.getElementById('var-type');
|
||
|
||
// Handle DB field
|
||
if (area === 'db') {
|
||
dbField.style.display = 'block';
|
||
dbInput.required = true;
|
||
} else {
|
||
dbField.style.display = 'none';
|
||
dbInput.required = false;
|
||
dbInput.value = 1; // Default value for non-DB areas
|
||
}
|
||
|
||
// Handle Bit field and data type restrictions
|
||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||
bitField.style.display = 'block';
|
||
// For bit areas, force data type to bool
|
||
typeSelect.value = 'bool';
|
||
// Disable other data types for bit areas
|
||
Array.from(typeSelect.options).forEach(option => {
|
||
option.disabled = (option.value !== 'bool');
|
||
});
|
||
} else {
|
||
bitField.style.display = 'none';
|
||
// Re-enable all data types for non-bit areas
|
||
Array.from(typeSelect.options).forEach(option => {
|
||
option.disabled = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Add variable to current dataset
|
||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected. Please select a dataset first.', 'error');
|
||
return;
|
||
}
|
||
|
||
const area = document.getElementById('var-area').value;
|
||
const data = {
|
||
name: document.getElementById('var-name').value,
|
||
area: area,
|
||
db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1,
|
||
offset: parseInt(document.getElementById('var-offset').value),
|
||
type: document.getElementById('var-type').value,
|
||
streaming: false // Default to not streaming
|
||
};
|
||
|
||
// Add bit parameter for bit areas
|
||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||
data.bit = parseInt(document.getElementById('var-bit').value);
|
||
}
|
||
|
||
fetch(`/api/datasets/${currentDatasetId}/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) {
|
||
document.getElementById('variable-form').reset();
|
||
loadDatasets(); // Reload to update counts
|
||
updateStatus();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Toggle streaming for variable in current dataset
|
||
function toggleStreaming(varName, enabled) {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected', 'error');
|
||
return;
|
||
}
|
||
|
||
fetch(`/api/datasets/${currentDatasetId}/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 from current dataset
|
||
function removeVariable(name) {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected', 'error');
|
||
return;
|
||
}
|
||
|
||
if (confirm(`Are you sure you want to remove the variable "${name}" from this dataset?`)) {
|
||
fetch(`/api/datasets/${currentDatasetId}/variables/${name}`, { method: 'DELETE' })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
showMessage(data.message, data.success ? 'success' : 'error');
|
||
if (data.success) {
|
||
loadDatasets(); // Reload to update counts
|
||
updateStatus();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 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();
|
||
});
|
||
});
|
||
|
||
// Status grid stop streaming button
|
||
document.getElementById('status-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');
|
||
});
|
||
});
|
||
|
||
// 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();
|
||
|
||
// Edit Variable Functions
|
||
let currentEditingVariable = null;
|
||
|
||
function editVariable(name) {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected', 'error');
|
||
return;
|
||
}
|
||
|
||
currentEditingVariable = name;
|
||
|
||
// Get variable data from current dataset
|
||
const dataset = currentDatasets[currentDatasetId];
|
||
if (dataset && dataset.variables && dataset.variables[name]) {
|
||
const variable = dataset.variables[name];
|
||
const streamingVars = dataset.streaming_variables || [];
|
||
|
||
// Create variable object with the same structure as the API
|
||
const variableData = {
|
||
name: name,
|
||
area: variable.area,
|
||
db: variable.db,
|
||
offset: variable.offset,
|
||
type: variable.type,
|
||
bit: variable.bit,
|
||
streaming: streamingVars.includes(name)
|
||
};
|
||
|
||
populateEditForm(variableData);
|
||
document.getElementById('edit-modal').style.display = 'block';
|
||
} else {
|
||
showMessage('Variable not found in current dataset', 'error');
|
||
}
|
||
}
|
||
|
||
function populateEditForm(variable) {
|
||
document.getElementById('edit-var-name').value = variable.name;
|
||
document.getElementById('edit-var-area').value = variable.area;
|
||
document.getElementById('edit-var-offset').value = variable.offset;
|
||
document.getElementById('edit-var-type').value = variable.type;
|
||
|
||
if (variable.db) {
|
||
document.getElementById('edit-var-db').value = variable.db;
|
||
}
|
||
|
||
if (variable.bit !== undefined) {
|
||
document.getElementById('edit-var-bit').value = variable.bit;
|
||
}
|
||
|
||
// Update field visibility based on area
|
||
toggleEditFields();
|
||
}
|
||
|
||
function toggleEditFields() {
|
||
const area = document.getElementById('edit-var-area').value;
|
||
const dbField = document.getElementById('edit-db-field');
|
||
const dbInput = document.getElementById('edit-var-db');
|
||
const bitField = document.getElementById('edit-bit-field');
|
||
const typeSelect = document.getElementById('edit-var-type');
|
||
|
||
// Handle DB field
|
||
if (area === 'db') {
|
||
dbField.style.display = 'block';
|
||
dbInput.required = true;
|
||
} else {
|
||
dbField.style.display = 'none';
|
||
dbInput.required = false;
|
||
dbInput.value = 1; // Default value for non-DB areas
|
||
}
|
||
|
||
// Handle Bit field and data type restrictions
|
||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||
bitField.style.display = 'block';
|
||
// For bit areas, force data type to bool
|
||
typeSelect.value = 'bool';
|
||
// Disable other data types for bit areas
|
||
Array.from(typeSelect.options).forEach(option => {
|
||
option.disabled = (option.value !== 'bool');
|
||
});
|
||
} else {
|
||
bitField.style.display = 'none';
|
||
// Re-enable all data types for non-bit areas
|
||
Array.from(typeSelect.options).forEach(option => {
|
||
option.disabled = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
function closeEditModal() {
|
||
document.getElementById('edit-modal').style.display = 'none';
|
||
currentEditingVariable = null;
|
||
}
|
||
|
||
// Handle edit form submission
|
||
document.getElementById('edit-variable-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
|
||
if (!currentEditingVariable || !currentDatasetId) {
|
||
showMessage('No variable or dataset selected for editing', 'error');
|
||
return;
|
||
}
|
||
|
||
const area = document.getElementById('edit-var-area').value;
|
||
const newName = document.getElementById('edit-var-name').value;
|
||
|
||
// First remove the old variable
|
||
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(deleteResult => {
|
||
if (deleteResult.success) {
|
||
// Then add the updated variable
|
||
const data = {
|
||
name: newName,
|
||
area: area,
|
||
db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1,
|
||
offset: parseInt(document.getElementById('edit-var-offset').value),
|
||
type: document.getElementById('edit-var-type').value,
|
||
streaming: false // Will be restored below if it was enabled
|
||
};
|
||
|
||
// Add bit parameter for bit areas
|
||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||
data.bit = parseInt(document.getElementById('edit-var-bit').value);
|
||
}
|
||
|
||
return fetch(`/api/datasets/${currentDatasetId}/variables`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
throw new Error(deleteResult.message);
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage('Variable updated successfully', 'success');
|
||
closeEditModal();
|
||
loadDatasets();
|
||
updateStatus();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage(`Error updating variable: ${error}`, 'error');
|
||
});
|
||
});
|
||
|
||
// Close modal when clicking outside of it
|
||
window.onclick = function (event) {
|
||
const editModal = document.getElementById('edit-modal');
|
||
const datasetModal = document.getElementById('dataset-modal');
|
||
if (event.target === editModal) {
|
||
closeEditModal();
|
||
}
|
||
if (event.target === datasetModal) {
|
||
datasetModal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Initialize field visibility on page load
|
||
toggleFields();
|
||
|
||
// ===== DATASET MANAGEMENT FUNCTIONS =====
|
||
|
||
// Load datasets on page load
|
||
loadDatasets();
|
||
|
||
// Dataset management variables
|
||
let currentDatasets = {};
|
||
let currentDatasetId = null;
|
||
|
||
// Load all datasets from API
|
||
function loadDatasets() {
|
||
fetch('/api/datasets')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
currentDatasets = data.datasets;
|
||
currentDatasetId = data.current_dataset_id;
|
||
updateDatasetSelector();
|
||
updateDatasetInfo();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading datasets:', error);
|
||
showMessage('Error loading datasets', 'error');
|
||
});
|
||
}
|
||
|
||
// Update the dataset selector dropdown
|
||
function updateDatasetSelector() {
|
||
const selector = document.getElementById('dataset-selector');
|
||
selector.innerHTML = '<option value="">Select a dataset...</option>';
|
||
|
||
Object.keys(currentDatasets).forEach(datasetId => {
|
||
const dataset = currentDatasets[datasetId];
|
||
const option = document.createElement('option');
|
||
option.value = datasetId;
|
||
option.textContent = `${dataset.name} (${dataset.prefix})`;
|
||
if (datasetId === currentDatasetId) {
|
||
option.selected = true;
|
||
}
|
||
selector.appendChild(option);
|
||
});
|
||
}
|
||
|
||
// Update dataset information display
|
||
function updateDatasetInfo() {
|
||
const infoSection = document.getElementById('dataset-info');
|
||
const actionsSection = document.getElementById('dataset-actions');
|
||
const variablesSection = document.getElementById('variables-section');
|
||
|
||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||
const dataset = currentDatasets[currentDatasetId];
|
||
|
||
// Show dataset info
|
||
document.getElementById('dataset-name').textContent = dataset.name;
|
||
document.getElementById('dataset-prefix').textContent = dataset.prefix;
|
||
document.getElementById('dataset-sampling').textContent =
|
||
dataset.sampling_interval ? `${dataset.sampling_interval}s` : 'Global interval';
|
||
document.getElementById('dataset-var-count').textContent = Object.keys(dataset.variables).length;
|
||
document.getElementById('dataset-stream-count').textContent = dataset.streaming_variables.length;
|
||
|
||
// Update dataset status
|
||
const statusSpan = document.getElementById('dataset-status');
|
||
const isActive = dataset.enabled;
|
||
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
|
||
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
|
||
|
||
// Update action buttons
|
||
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
|
||
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
|
||
|
||
// Show sections
|
||
infoSection.style.display = 'block';
|
||
actionsSection.style.display = 'block';
|
||
variablesSection.style.display = 'block';
|
||
|
||
// Update variables section title
|
||
document.getElementById('current-dataset-title').textContent = dataset.name;
|
||
|
||
// Load variables for this dataset
|
||
loadDatasetVariables(currentDatasetId);
|
||
} else {
|
||
infoSection.style.display = 'none';
|
||
actionsSection.style.display = 'none';
|
||
variablesSection.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Load variables for a specific dataset
|
||
function loadDatasetVariables(datasetId) {
|
||
if (!datasetId || !currentDatasets[datasetId]) {
|
||
// Clear the table if no valid dataset
|
||
document.getElementById('variables-tbody').innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const dataset = currentDatasets[datasetId];
|
||
const variables = dataset.variables || {};
|
||
const streamingVars = dataset.streaming_variables || [];
|
||
const tbody = document.getElementById('variables-tbody');
|
||
|
||
// Clear existing rows
|
||
tbody.innerHTML = '';
|
||
|
||
// Add a row for each variable
|
||
Object.keys(variables).forEach(varName => {
|
||
const variable = variables[varName];
|
||
const row = document.createElement('tr');
|
||
|
||
// Format memory area display
|
||
let memoryAreaDisplay = '';
|
||
if (variable.area === 'db') {
|
||
memoryAreaDisplay = `DB${variable.db || 'N/A'}.${variable.offset}`;
|
||
} else if (variable.area === 'mw' || variable.area === 'm') {
|
||
memoryAreaDisplay = `MW${variable.offset}`;
|
||
} else if (variable.area === 'pew' || variable.area === 'pe') {
|
||
memoryAreaDisplay = `PEW${variable.offset}`;
|
||
} else if (variable.area === 'paw' || variable.area === 'pa') {
|
||
memoryAreaDisplay = `PAW${variable.offset}`;
|
||
} else if (variable.area === 'e') {
|
||
memoryAreaDisplay = `E${variable.offset}.${variable.bit}`;
|
||
} else if (variable.area === 'a') {
|
||
memoryAreaDisplay = `A${variable.offset}.${variable.bit}`;
|
||
} else if (variable.area === 'mb') {
|
||
memoryAreaDisplay = `M${variable.offset}.${variable.bit}`;
|
||
} else {
|
||
memoryAreaDisplay = `${variable.area.toUpperCase()}${variable.offset}`;
|
||
}
|
||
|
||
// Check if variable is in streaming list
|
||
const isStreaming = streamingVars.includes(varName);
|
||
|
||
row.innerHTML = `
|
||
<td>${varName}</td>
|
||
<td>${memoryAreaDisplay}</td>
|
||
<td>${variable.offset}</td>
|
||
<td>${variable.type.toUpperCase()}</td>
|
||
<td>
|
||
<label>
|
||
<input type="checkbox" id="stream-${varName}" role="switch" ${isStreaming ? 'checked' : ''}
|
||
onchange="toggleStreaming('${varName}', this.checked)">
|
||
Enable
|
||
</label>
|
||
</td>
|
||
<td>
|
||
<button class="outline" onclick="editVariable('${varName}')">✏️ Edit</button>
|
||
<button class="secondary" onclick="removeVariable('${varName}')">🗑️ Remove</button>
|
||
</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
|
||
// Dataset selector change handler
|
||
document.getElementById('dataset-selector').addEventListener('change', function () {
|
||
const selectedDatasetId = this.value;
|
||
if (selectedDatasetId) {
|
||
// Set as current dataset
|
||
fetch('/api/datasets/current', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ dataset_id: selectedDatasetId })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
currentDatasetId = selectedDatasetId;
|
||
// Reload datasets to get fresh data, then update info
|
||
loadDatasets();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error setting current dataset', 'error');
|
||
});
|
||
} else {
|
||
currentDatasetId = null;
|
||
updateDatasetInfo();
|
||
}
|
||
});
|
||
|
||
// New dataset button
|
||
document.getElementById('new-dataset-btn').addEventListener('click', function () {
|
||
document.getElementById('dataset-modal').style.display = 'block';
|
||
});
|
||
|
||
// Close dataset modal
|
||
document.getElementById('close-dataset-modal').addEventListener('click', function () {
|
||
document.getElementById('dataset-modal').style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('cancel-dataset-btn').addEventListener('click', function () {
|
||
document.getElementById('dataset-modal').style.display = 'none';
|
||
});
|
||
|
||
// Create new dataset
|
||
document.getElementById('dataset-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
|
||
const data = {
|
||
dataset_id: document.getElementById('dataset-id').value.trim(),
|
||
name: document.getElementById('dataset-name-input').value.trim(),
|
||
prefix: document.getElementById('dataset-prefix-input').value.trim(),
|
||
sampling_interval: document.getElementById('dataset-sampling-input').value || null
|
||
};
|
||
|
||
if (data.sampling_interval) {
|
||
data.sampling_interval = parseFloat(data.sampling_interval);
|
||
}
|
||
|
||
fetch('/api/datasets', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage(data.message, 'success');
|
||
document.getElementById('dataset-modal').style.display = 'none';
|
||
document.getElementById('dataset-form').reset();
|
||
loadDatasets();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error creating dataset', 'error');
|
||
});
|
||
});
|
||
|
||
// Delete dataset button
|
||
document.getElementById('delete-dataset-btn').addEventListener('click', function () {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected', 'error');
|
||
return;
|
||
}
|
||
|
||
const dataset = currentDatasets[currentDatasetId];
|
||
if (confirm(`Are you sure you want to delete dataset "${dataset.name}"?`)) {
|
||
fetch(`/api/datasets/${currentDatasetId}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage(data.message, 'success');
|
||
loadDatasets();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error deleting dataset', 'error');
|
||
});
|
||
}
|
||
});
|
||
|
||
// Activate dataset button
|
||
document.getElementById('activate-dataset-btn').addEventListener('click', function () {
|
||
if (!currentDatasetId) return;
|
||
|
||
fetch(`/api/datasets/${currentDatasetId}/activate`, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage(data.message, 'success');
|
||
loadDatasets();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error activating dataset', 'error');
|
||
});
|
||
});
|
||
|
||
// Deactivate dataset button
|
||
document.getElementById('deactivate-dataset-btn').addEventListener('click', function () {
|
||
if (!currentDatasetId) return;
|
||
|
||
fetch(`/api/datasets/${currentDatasetId}/deactivate`, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage(data.message, 'success');
|
||
loadDatasets();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error deactivating dataset', 'error');
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |