2606 lines
111 KiB
HTML
2606 lines
111 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;
|
||
}
|
||
|
||
/* Variable value cells styling */
|
||
.variables-table td[id^="value-"] {
|
||
font-family: var(--pico-font-family-monospace);
|
||
font-weight: bold;
|
||
text-align: center;
|
||
min-width: 100px;
|
||
}
|
||
|
||
/* Refresh button styling */
|
||
#refresh-values-btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Diagnose button styling */
|
||
#diagnose-btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Last refresh time styling */
|
||
#last-refresh-time {
|
||
font-style: italic;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Error cell tooltips */
|
||
.variables-table td[id^="value-"]:hover {
|
||
position: relative;
|
||
}
|
||
|
||
/* Variable value status colors */
|
||
.value-success {
|
||
color: var(--pico-color-green-600) !important;
|
||
}
|
||
|
||
.value-error {
|
||
color: var(--pico-color-red-500) !important;
|
||
}
|
||
|
||
.value-warning {
|
||
color: var(--pico-color-amber-600) !important;
|
||
}
|
||
|
||
.value-offline {
|
||
color: var(--pico-muted-color) !important;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
.alert-warning {
|
||
background-color: var(--pico-color-amber-100);
|
||
color: var(--pico-color-amber-800);
|
||
border: 1px solid var(--pico-color-amber-300);
|
||
}
|
||
|
||
.alert-info {
|
||
background-color: var(--pico-color-blue-100);
|
||
color: var(--pico-color-blue-800);
|
||
border: 1px solid var(--pico-color-blue-300);
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* CSV Configuration styles */
|
||
.csv-config-display {
|
||
background: var(--pico-card-background-color);
|
||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.config-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.config-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem 0;
|
||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||
}
|
||
|
||
.config-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.config-item span {
|
||
font-weight: normal;
|
||
color: var(--pico-primary);
|
||
}
|
||
|
||
.directory-stats {
|
||
padding: 1rem;
|
||
background: var(--pico-muted-background-color);
|
||
border-radius: var(--pico-border-radius);
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.directory-stats .stat-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.day-folder-item {
|
||
background: var(--pico-card-background-color);
|
||
border: 1px solid var(--pico-border-color);
|
||
border-radius: var(--pico-border-radius);
|
||
padding: 0.75rem;
|
||
margin: 0.5rem 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.cleanup-status {
|
||
padding: 0.5rem;
|
||
border-radius: var(--pico-border-radius);
|
||
margin: 0.5rem 0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.cleanup-success {
|
||
background-color: var(--pico-color-green-100);
|
||
color: var(--pico-color-green-800);
|
||
border: 1px solid var(--pico-color-green-300);
|
||
}
|
||
|
||
.cleanup-error {
|
||
background-color: var(--pico-color-red-100);
|
||
color: var(--pico-color-red-800);
|
||
border: 1px solid var(--pico-color-red-300);
|
||
}
|
||
</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>
|
||
|
||
<!-- 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;">
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<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');
|
||
let alertClass;
|
||
|
||
switch (type) {
|
||
case 'success':
|
||
alertClass = 'alert-success';
|
||
break;
|
||
case 'warning':
|
||
alertClass = 'alert-warning';
|
||
break;
|
||
case 'info':
|
||
alertClass = 'alert-info';
|
||
break;
|
||
case 'error':
|
||
default:
|
||
alertClass = 'alert-error';
|
||
break;
|
||
}
|
||
|
||
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');
|
||
});
|
||
}
|
||
|
||
// Refresh variable values from PLC
|
||
function refreshVariableValues() {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected', 'error');
|
||
return;
|
||
}
|
||
|
||
const refreshBtn = document.getElementById('refresh-values-btn');
|
||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||
|
||
// Disable button and show loading state
|
||
refreshBtn.disabled = true;
|
||
refreshBtn.innerHTML = '⏳ Reading...';
|
||
|
||
fetch(`/api/datasets/${currentDatasetId}/variables/values`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Update variable values in the table
|
||
Object.keys(data.values).forEach(varName => {
|
||
const valueCell = document.getElementById(`value-${varName}`);
|
||
if (valueCell) {
|
||
const value = data.values[varName];
|
||
valueCell.textContent = value;
|
||
|
||
// Color coding and tooltip based on value status
|
||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||
|
||
// Add tooltip with detailed error if available
|
||
const errorDetail = data.detailed_errors && data.detailed_errors[varName];
|
||
if (errorDetail) {
|
||
valueCell.title = `Error: ${errorDetail}`;
|
||
valueCell.style.cursor = 'help';
|
||
}
|
||
} else {
|
||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||
valueCell.title = `Value: ${value}`;
|
||
valueCell.style.cursor = 'default';
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update timestamp, stats, and source information
|
||
if (data.timestamp) {
|
||
const stats = data.stats;
|
||
const source = data.source || 'unknown';
|
||
const isCache = data.is_cached;
|
||
|
||
// Create source indicator
|
||
let sourceIcon = '';
|
||
let sourceText = '';
|
||
if (isCache) {
|
||
sourceIcon = '📊';
|
||
sourceText = 'from streaming cache';
|
||
} else if (source === 'plc_direct') {
|
||
sourceIcon = '🔗';
|
||
sourceText = 'direct PLC read';
|
||
} else {
|
||
sourceIcon = '❓';
|
||
sourceText = 'unknown source';
|
||
}
|
||
|
||
if (stats && stats.failed > 0) {
|
||
lastRefreshTime.innerHTML = `
|
||
Last refresh: ${data.timestamp}<br/>
|
||
<small style="color: var(--pico-color-amber-600);">
|
||
⚠️ ${stats.success}/${stats.total} variables read (${stats.failed} failed)
|
||
</small><br/>
|
||
<small style="color: var(--pico-muted-color);">
|
||
${sourceIcon} ${sourceText}
|
||
</small>
|
||
`;
|
||
} else {
|
||
lastRefreshTime.innerHTML = `
|
||
Last refresh: ${data.timestamp}<br/>
|
||
<small style="color: var(--pico-color-green-600);">
|
||
✅ All ${stats ? stats.success : 'N/A'} variables read successfully
|
||
</small><br/>
|
||
<small style="color: var(--pico-muted-color);">
|
||
${sourceIcon} ${sourceText}
|
||
</small>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Show appropriate message
|
||
if (data.warning) {
|
||
showMessage(data.warning, 'warning');
|
||
// Show detailed error information in console for debugging
|
||
if (data.detailed_errors && Object.keys(data.detailed_errors).length > 0) {
|
||
console.warn('Variable read errors:', data.detailed_errors);
|
||
}
|
||
} else {
|
||
showMessage(data.message, 'success');
|
||
}
|
||
|
||
} else {
|
||
// Complete failure case
|
||
showMessage(data.message, 'error');
|
||
clearVariableValues('ERROR');
|
||
|
||
// Update timestamp with error info
|
||
const source = data.source || 'unknown';
|
||
const isCache = data.is_cached;
|
||
|
||
let sourceIcon = '';
|
||
let sourceText = '';
|
||
if (isCache) {
|
||
sourceIcon = '📊';
|
||
sourceText = 'from streaming cache';
|
||
} else if (source === 'plc_direct') {
|
||
sourceIcon = '🔗';
|
||
sourceText = 'direct PLC read';
|
||
} else {
|
||
sourceIcon = '❓';
|
||
sourceText = 'unknown source';
|
||
}
|
||
|
||
lastRefreshTime.innerHTML = `
|
||
Last refresh attempt: ${data.timestamp}<br/>
|
||
<small style="color: var(--pico-color-red-500);">
|
||
❌ Failed to read any variables
|
||
</small><br/>
|
||
<small style="color: var(--pico-muted-color);">
|
||
${sourceIcon} ${sourceText}
|
||
</small>
|
||
`;
|
||
|
||
// Show detailed error information if available
|
||
if (data.detailed_errors && Object.keys(data.detailed_errors).length > 0) {
|
||
console.error('Detailed variable errors:', data.detailed_errors);
|
||
|
||
// Create a detailed error message for the user
|
||
const errorSummary = Object.keys(data.detailed_errors).map(varName =>
|
||
`${varName}: ${data.detailed_errors[varName]}`
|
||
).join('\n');
|
||
|
||
// Show in console and optionally as a detailed error dialog
|
||
console.error('Variable Error Details:\n' + errorSummary);
|
||
}
|
||
|
||
// Handle specific error types
|
||
if (data.error_type === 'connection_error') {
|
||
clearVariableValues('PLC OFFLINE');
|
||
} else if (data.error_type === 'all_failed') {
|
||
clearVariableValues('READ FAILED');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error refreshing variable values:', error);
|
||
showMessage('Network error reading variable values from PLC', 'error');
|
||
clearVariableValues('COMM ERROR');
|
||
|
||
lastRefreshTime.innerHTML = `
|
||
Last refresh attempt: ${new Date().toLocaleString()}<br/>
|
||
<small style="color: var(--pico-color-red-500);">
|
||
❌ Network/Communication error
|
||
</small>
|
||
`;
|
||
})
|
||
.finally(() => {
|
||
// Re-enable button
|
||
refreshBtn.disabled = false;
|
||
refreshBtn.innerHTML = '🔄 Refresh Values';
|
||
});
|
||
}
|
||
|
||
// Clear all variable values and set status message
|
||
function clearVariableValues(statusMessage = '--') {
|
||
// Find all value cells and clear them
|
||
const valueCells = document.querySelectorAll('[id^="value-"]');
|
||
valueCells.forEach(cell => {
|
||
cell.textContent = statusMessage;
|
||
cell.style.color = 'var(--pico-muted-color)';
|
||
});
|
||
}
|
||
|
||
// Auto-refresh values when dataset changes (optional)
|
||
function autoRefreshOnDatasetChange() {
|
||
if (currentDatasetId) {
|
||
// Small delay to ensure table is loaded
|
||
setTimeout(() => {
|
||
refreshVariableValues();
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
// Diagnostic function for connection and variable issues
|
||
function diagnoseConnection() {
|
||
if (!currentDatasetId) {
|
||
showMessage('No dataset selected for diagnosis', 'error');
|
||
return;
|
||
}
|
||
|
||
const diagnoseBtn = document.getElementById('diagnose-btn');
|
||
const originalText = diagnoseBtn.innerHTML;
|
||
|
||
// Disable button and show diagnostic state
|
||
diagnoseBtn.disabled = true;
|
||
diagnoseBtn.innerHTML = '🔍 Diagnosing...';
|
||
|
||
// Create diagnostic report
|
||
let diagnosticReport = [];
|
||
|
||
diagnosticReport.push('=== PLC CONNECTION DIAGNOSTICS ===');
|
||
diagnosticReport.push(`Dataset: ${currentDatasetId}`);
|
||
diagnosticReport.push(`Timestamp: ${new Date().toLocaleString()}`);
|
||
diagnosticReport.push('');
|
||
|
||
// Step 1: Check PLC connection status
|
||
fetch('/api/status')
|
||
.then(response => response.json())
|
||
.then(statusData => {
|
||
diagnosticReport.push('1. PLC Connection Status:');
|
||
diagnosticReport.push(` Connected: ${statusData.plc_connected ? 'YES' : 'NO'}`);
|
||
diagnosticReport.push(` PLC IP: ${statusData.plc_config.ip}`);
|
||
diagnosticReport.push(` Rack: ${statusData.plc_config.rack}`);
|
||
diagnosticReport.push(` Slot: ${statusData.plc_config.slot}`);
|
||
diagnosticReport.push('');
|
||
|
||
if (!statusData.plc_connected) {
|
||
diagnosticReport.push(' ❌ PLC is not connected. Please check:');
|
||
diagnosticReport.push(' - Network connectivity to PLC');
|
||
diagnosticReport.push(' - PLC IP address, rack, and slot configuration');
|
||
diagnosticReport.push(' - PLC is powered on and operational');
|
||
diagnosticReport.push('');
|
||
showDiagnosticResults(diagnosticReport);
|
||
return;
|
||
}
|
||
|
||
// Step 2: Get dataset information
|
||
return fetch('/api/datasets')
|
||
.then(response => response.json())
|
||
.then(datasetData => {
|
||
const dataset = datasetData.datasets[currentDatasetId];
|
||
if (!dataset) {
|
||
diagnosticReport.push('2. Dataset Status:');
|
||
diagnosticReport.push(' ❌ Dataset not found');
|
||
showDiagnosticResults(diagnosticReport);
|
||
return;
|
||
}
|
||
|
||
diagnosticReport.push('2. Dataset Information:');
|
||
diagnosticReport.push(` Name: ${dataset.name}`);
|
||
diagnosticReport.push(` Variables: ${Object.keys(dataset.variables).length}`);
|
||
diagnosticReport.push(` Active: ${dataset.enabled ? 'YES' : 'NO'}`);
|
||
diagnosticReport.push('');
|
||
|
||
// Step 3: Test variable reading with diagnostics
|
||
diagnosticReport.push('3. Variable Reading Test:');
|
||
return fetch(`/api/datasets/${currentDatasetId}/variables/values`)
|
||
.then(response => response.json())
|
||
.then(valueData => {
|
||
if (valueData.success) {
|
||
const stats = valueData.stats || {};
|
||
diagnosticReport.push(` ✅ Success: ${stats.success || 0}/${stats.total || 0} variables read`);
|
||
|
||
if (stats.failed > 0) {
|
||
diagnosticReport.push(` ⚠️ Failed: ${stats.failed} variables had errors`);
|
||
diagnosticReport.push('');
|
||
diagnosticReport.push('4. Variable-Specific Errors:');
|
||
|
||
if (valueData.detailed_errors) {
|
||
Object.keys(valueData.detailed_errors).forEach(varName => {
|
||
diagnosticReport.push(` ${varName}: ${valueData.detailed_errors[varName]}`);
|
||
});
|
||
}
|
||
} else {
|
||
diagnosticReport.push(' ✅ All variables read successfully');
|
||
}
|
||
} else {
|
||
diagnosticReport.push(` ❌ Complete failure: ${valueData.message}`);
|
||
diagnosticReport.push('');
|
||
diagnosticReport.push('4. Detailed Error Information:');
|
||
|
||
if (valueData.detailed_errors) {
|
||
Object.keys(valueData.detailed_errors).forEach(varName => {
|
||
diagnosticReport.push(` ${varName}: ${valueData.detailed_errors[varName]}`);
|
||
});
|
||
}
|
||
|
||
diagnosticReport.push('');
|
||
diagnosticReport.push('5. Troubleshooting Suggestions:');
|
||
if (valueData.error_type === 'connection_error') {
|
||
diagnosticReport.push(' - Check PLC network connection');
|
||
diagnosticReport.push(' - Verify PLC is responding to network requests');
|
||
diagnosticReport.push(' - Check firewall settings');
|
||
} else if (valueData.error_type === 'all_failed') {
|
||
diagnosticReport.push(' - Verify variable memory addresses are correct');
|
||
diagnosticReport.push(' - Check if data blocks exist in PLC program');
|
||
diagnosticReport.push(' - Ensure variable types match PLC configuration');
|
||
}
|
||
}
|
||
|
||
showDiagnosticResults(diagnosticReport);
|
||
});
|
||
});
|
||
})
|
||
.catch(error => {
|
||
diagnosticReport.push('❌ Diagnostic failed with network error:');
|
||
diagnosticReport.push(` ${error.message}`);
|
||
diagnosticReport.push('');
|
||
diagnosticReport.push('Troubleshooting:');
|
||
diagnosticReport.push(' - Check web server connection');
|
||
diagnosticReport.push(' - Refresh the page and try again');
|
||
showDiagnosticResults(diagnosticReport);
|
||
})
|
||
.finally(() => {
|
||
// Re-enable button
|
||
diagnoseBtn.disabled = false;
|
||
diagnoseBtn.innerHTML = originalText;
|
||
});
|
||
}
|
||
|
||
// Show diagnostic results in console and as a message
|
||
function showDiagnosticResults(diagnosticReport) {
|
||
const reportText = diagnosticReport.join('\n');
|
||
|
||
// Log to console for detailed analysis
|
||
console.log(reportText);
|
||
|
||
// Show summary message to user
|
||
const errorCount = reportText.match(/❌/g)?.length || 0;
|
||
const warningCount = reportText.match(/⚠️/g)?.length || 0;
|
||
const successCount = reportText.match(/✅/g)?.length || 0;
|
||
|
||
let summaryMessage = 'Diagnosis completed. ';
|
||
if (errorCount > 0) {
|
||
summaryMessage += `${errorCount} errors found. `;
|
||
}
|
||
if (warningCount > 0) {
|
||
summaryMessage += `${warningCount} warnings found. `;
|
||
}
|
||
if (successCount > 0) {
|
||
summaryMessage += `${successCount} checks passed. `;
|
||
}
|
||
summaryMessage += 'Check browser console (F12) for detailed report.';
|
||
|
||
const messageType = errorCount > 0 ? 'error' : (warningCount > 0 ? 'warning' : 'success');
|
||
showMessage(summaryMessage, messageType);
|
||
}
|
||
|
||
// 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 statusBar = document.getElementById('dataset-status-bar');
|
||
const variablesManagement = document.getElementById('variables-management');
|
||
const noDatasetMessage = document.getElementById('no-dataset-message');
|
||
|
||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||
const dataset = currentDatasets[currentDatasetId];
|
||
|
||
// Show dataset info in status bar
|
||
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
|
||
statusBar.style.display = 'block';
|
||
variablesManagement.style.display = 'block';
|
||
noDatasetMessage.style.display = 'none';
|
||
|
||
// Load variables for this dataset
|
||
loadDatasetVariables(currentDatasetId);
|
||
} else {
|
||
statusBar.style.display = 'none';
|
||
variablesManagement.style.display = 'none';
|
||
noDatasetMessage.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// 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 id="value-${varName}" style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||
--
|
||
</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();
|
||
// Auto-refresh values for the new dataset
|
||
autoRefreshOnDatasetChange();
|
||
} else {
|
||
showMessage(data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error setting current dataset', 'error');
|
||
});
|
||
} else {
|
||
currentDatasetId = null;
|
||
updateDatasetInfo();
|
||
// Clear values when no dataset is selected
|
||
clearVariableValues();
|
||
}
|
||
});
|
||
|
||
// 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');
|
||
});
|
||
});
|
||
|
||
// CSV Configuration Functions
|
||
function loadCsvConfig() {
|
||
fetch('/api/csv/config')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const config = data.config;
|
||
|
||
// Update display elements
|
||
document.getElementById('csv-directory-path').textContent = config.current_directory || 'N/A';
|
||
document.getElementById('csv-rotation-enabled').textContent = config.rotation_enabled ? '✅ Yes' : '❌ No';
|
||
document.getElementById('csv-max-size').textContent = config.max_size_mb ? `${config.max_size_mb} MB` : 'No limit';
|
||
document.getElementById('csv-max-days').textContent = config.max_days ? `${config.max_days} days` : 'No limit';
|
||
document.getElementById('csv-max-hours').textContent = config.max_hours ? `${config.max_hours} hours` : 'No limit';
|
||
document.getElementById('csv-cleanup-interval').textContent = `${config.cleanup_interval_hours} hours`;
|
||
|
||
// Update form fields
|
||
document.getElementById('records-directory').value = config.records_directory || '';
|
||
document.getElementById('rotation-enabled').checked = config.rotation_enabled || false;
|
||
document.getElementById('max-size-mb').value = config.max_size_mb || '';
|
||
document.getElementById('max-days').value = config.max_days || '';
|
||
document.getElementById('max-hours').value = config.max_hours || '';
|
||
document.getElementById('cleanup-interval').value = config.cleanup_interval_hours || 24;
|
||
|
||
// Load directory information
|
||
loadCsvDirectoryInfo();
|
||
} else {
|
||
showMessage('Error loading CSV configuration: ' + data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error loading CSV configuration', 'error');
|
||
});
|
||
}
|
||
|
||
function loadCsvDirectoryInfo() {
|
||
fetch('/api/csv/directory/info')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
const info = data.info;
|
||
const statsDiv = document.getElementById('directory-stats');
|
||
|
||
let html = `
|
||
<div class="stat-item">
|
||
<strong>📁 Directory:</strong>
|
||
<span>${info.base_directory}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<strong>📊 Total Files:</strong>
|
||
<span>${info.total_files}</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<strong>💾 Total Size:</strong>
|
||
<span>${info.total_size_mb} MB</span>
|
||
</div>
|
||
`;
|
||
|
||
if (info.oldest_file) {
|
||
html += `
|
||
<div class="stat-item">
|
||
<strong>📅 Oldest File:</strong>
|
||
<span>${new Date(info.oldest_file).toLocaleString()}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (info.newest_file) {
|
||
html += `
|
||
<div class="stat-item">
|
||
<strong>🆕 Newest File:</strong>
|
||
<span>${new Date(info.newest_file).toLocaleString()}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (info.day_folders && info.day_folders.length > 0) {
|
||
html += '<h4>📂 Day Folders:</h4>';
|
||
info.day_folders.forEach(folder => {
|
||
html += `
|
||
<div class="day-folder-item">
|
||
<span><strong>${folder.name}</strong></span>
|
||
<span>${folder.files} files, ${folder.size_mb} MB</span>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
statsDiv.innerHTML = html;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
document.getElementById('directory-stats').innerHTML = '<p>Error loading directory information</p>';
|
||
});
|
||
}
|
||
|
||
function triggerManualCleanup() {
|
||
if (!confirm('¿Estás seguro de que quieres ejecutar la limpieza manual? Esto eliminará archivos antiguos según la configuración actual.')) {
|
||
return;
|
||
}
|
||
|
||
fetch('/api/csv/cleanup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage('Limpieza ejecutada correctamente', 'success');
|
||
loadCsvDirectoryInfo(); // Reload directory info
|
||
} else {
|
||
showMessage('Error en la limpieza: ' + data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error ejecutando la limpieza', 'error');
|
||
});
|
||
}
|
||
|
||
// CSV Configuration form handler
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Load CSV configuration on page load
|
||
loadCsvConfig();
|
||
|
||
// Handle CSV config form submission
|
||
document.getElementById('csv-config-form').addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(e.target);
|
||
const configData = {};
|
||
|
||
// Convert form data to object, handling empty values
|
||
for (let [key, value] of formData.entries()) {
|
||
if (key === 'rotation_enabled') {
|
||
configData[key] = document.getElementById('rotation-enabled').checked;
|
||
} else if (value.trim() === '') {
|
||
configData[key] = null;
|
||
} else if (key.includes('max_') || key.includes('cleanup_interval')) {
|
||
configData[key] = parseFloat(value) || null;
|
||
} else {
|
||
configData[key] = value.trim();
|
||
}
|
||
}
|
||
|
||
fetch('/api/csv/config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(configData)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage('Configuración CSV actualizada correctamente', 'success');
|
||
loadCsvConfig(); // Reload to show updated values
|
||
} else {
|
||
showMessage('Error actualizando configuración CSV: ' + data.message, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage('Error actualizando configuración CSV', 'error');
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |