Loading noctua/web/pages/macros/widgets.html +73 −0 Original line number Diff line number Diff line Loading @@ -327,3 +327,76 @@ {% macro monitor_val_template() %} <span class="val-node"></span> {% endmacro %} {# --- WIDGET: Telemetry Card with Dynamic Fields --- #} {% macro widget_telemetry_card(label, fields) %} {% set safe_id = label | replace(' ', '-') | lower %} <div class="col-md-6" id="container-{{ safe_id }}"> <div class="card h-100 border-secondary shadow-sm"> <div class="card-header bg-dark d-flex justify-content-between align-items-center"> <span class="text-capitalize fw-bold">{{ label }}</span> <small class="text-muted timer">just now</small> </div> <div class="card-body p-0"> <!-- Raw JSON pre-renderer --> <pre class="p-3 m-0 text-light bg-black d-none" id="data-{{ safe_id }}" style="font-size: 0.75rem; white-space: pre-wrap;"></pre> <div id="pretty-{{ safe_id }}" class="pretty-container"> <table class="table table-dark mb-0 table-sm align-middle" style="font-size: 0.9rem;"> <thead> <tr class="text-muted small border-bottom border-secondary"> <th class="ps-3" style="width: 35%;">parameter</th> <th style="width: 50%;">status</th> <th style="width: 15%;" class="text-center">err</th> </tr> </thead> <tbody> {% for field in fields %} <tr class="border-bottom border-secondary border-opacity-25"> <td class="ps-3 fw-semibold py-2">{{ field.label }}</td> <td class="py-2"> {% if field.subfields %} <!-- Nested Object: render subtable key-value --> <table class="table table-sm table-borderless m-0 p-0" style="background: transparent;"> <tbody> {% for sub in field.subfields %} <tr> <td class="text-muted p-0 pe-2 small" style="width: 35%;">{{ sub.label }}</td> <td class="p-0"> {% if sub.is_array2 %} <div class="d-flex gap-3 font-monospace"> <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}-0">N/A</span> <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}-1">N/A</span> </div> {% else %} <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}">N/A</span> {% endif %} </td> </tr> {% endfor %} </tbody> </table> {% else %} <!-- Flat Field --> {% if field.is_array2 %} <div class="d-flex gap-3 font-monospace"> <span data-status="{{ safe_id }}-{{ field.endpoint }}-0">N/A</span> <span data-status="{{ safe_id }}-{{ field.endpoint }}-1">N/A</span> </div> {% else %} <span class="badge bg-secondary" data-status="{{ safe_id }}-{{ field.endpoint }}">N/A</span> {% endif %} {% endif %} </td> <td class="text-center py-2"> <span class="badge bg-success" data-status="{{ safe_id }}-{{ field.endpoint }}-error">OK</span> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> {% endmacro %} noctua/web/pages/status.html +6 −6 Original line number Diff line number Diff line Loading @@ -21,15 +21,15 @@ </div> </div> <!-- Ordered containers --> <!-- Dynamic monitor cards populated automatically from WebSocket payload --> <div id="status-container" class="row g-3"> {{ w.widget_monitor(label="dome", safe_id="dome") }} {{ w.widget_monitor(label="stage", safe_id="stage") }} {{ w.widget_monitor(label="camera", safe_id="camera") }} {{ w.widget_monitor(label="telescope", safe_id="telescope") }} {{ w.widget_monitor("dome", "dome") }} {{ w.widget_monitor("stage", "stage") }} {{ w.widget_monitor("camera", "camera") }} {{ w.widget_monitor("telescope", "telescope") }} </div> <!-- Blueprints for dynamic parts --> <!-- Blueprints for dynamic parts (Cloned by JS, no HTML strings in JS) --> <template id="table-blueprint">{{ w.monitor_table_template() }}</template> <template id="row-blueprint">{{ w.monitor_row_template() }}</template> <template id="subtable-blueprint">{{ w.monitor_subtable_template() }}</template> Loading noctua/web/static/js/status-view.js +19 −19 Original line number Diff line number Diff line // status-view.js // Renders subsystem tables dynamically, splitting 2-element arrays into two lines and objects into structured sub-tables. // Dynamic telemetry update loop. Clones HTML blueprints and maps keys as labels. document.addEventListener('DOMContentLoaded', () => { const tableBlueprint = document.getElementById('table-blueprint'); Loading @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { const globalRawCheckbox = document.getElementById('global-raw'); const globalForceCheckbox = document.getElementById('global-force'); // Central state cache to remember previous telemetry values for change detection // Central state cache to remember previous telemetry values for change-detection pulsing const previousState = {}; Loading Loading @@ -72,18 +72,18 @@ document.addEventListener('DOMContentLoaded', () => { const showRaw = globalRawCheckbox ? globalRawCheckbox.checked : false; // Initialize state cache for the subsystem if missing if (!previousState[subsystem]) { previousState[subsystem] = {}; } // Render raw JSON display if (dataPre) { dataPre.textContent = JSON.stringify(data, null, 4); dataPre.classList.toggle('d-none', !showRaw); } // 1. One-time table DOM assembly (builds the structure once) // Initialize state cache for the subsystem if missing if (!previousState[subsystem]) { previousState[subsystem] = {}; } // 1. One-time table DOM assembly (only runs on first websocket message for this subsystem) if (prettyDiv && prettyDiv.children.length === 0 && tableBlueprint && rowBlueprint) { prettyDiv.classList.toggle('d-none', showRaw); Loading @@ -92,7 +92,9 @@ document.addEventListener('DOMContentLoaded', () => { Object.entries(data).forEach(([deviceKey, val]) => { const rowClone = rowBlueprint.content.cloneNode(true); rowClone.querySelector('.device-name').textContent = deviceKey.replace('_', ' '); // NO EXPLICIT LABELS: Automatically use the JSON key as label, replacing underscores rowClone.querySelector('.device-name').textContent = deviceKey.replace(/_/g, ' '); const statusTd = rowClone.querySelector('.device-status'); const errTd = rowClone.querySelector('.device-err'); Loading @@ -112,7 +114,8 @@ document.addEventListener('DOMContentLoaded', () => { Object.entries(responseValue).forEach(([k, v]) => { const subrowClone = subrowBlueprint.content.cloneNode(true); subrowClone.querySelector('.key-cell').textContent = k.replace('_', ' '); // Use sub-key as label subrowClone.querySelector('.key-cell').textContent = k.replace(/_/g, ' '); // Bind nested status attribute const valCell = subrowClone.querySelector('.val-cell'); Loading Loading @@ -149,13 +152,13 @@ document.addEventListener('DOMContentLoaded', () => { let finalValue = 'N/A'; // Special case: update error badge cell if (subProperty === 'error') { if (statusKey.endsWith('-error')) { const errors = endpointData.error; const hasErrors = errors && errors.length > 0; const displayValue = hasErrors ? 'ERR' : 'OK'; const prevValue = el.textContent; const hasChanged = prevValue !== '' && prevValue !== 'N/A' && prevValue !== displayValue; const hasChanged = prevValue !== '' && prevValue !== displayValue; if (hasChanged) { el.classList.add('pulse-update'); Loading @@ -164,11 +167,8 @@ document.addEventListener('DOMContentLoaded', () => { el.textContent = displayValue; el.className = `badge ${hasErrors ? 'bg-danger' : 'bg-success'}`; if (hasErrors) { el.setAttribute('title', errors.join(', ')); } else { el.removeAttribute('title'); } if (hasErrors) el.setAttribute('title', errors.join(', ')); else el.removeAttribute('title'); return; } Loading @@ -189,12 +189,12 @@ document.addEventListener('DOMContentLoaded', () => { finalValue = 'N/A'; } // Dynamic layout generation: if value is a 2-element array, split on two lines // Dynamic layout: if value is a 2-element array, show side-by-side with a gap let displayHTML = ''; const isArray2 = Array.isArray(finalValue) && finalValue.length === 2; if (isArray2) { displayHTML = `<div class="small fw-bold">${finalValue[0]}</div><div class="small fw-bold">${finalValue[1]}</div>`; displayHTML = `<div class="d-flex gap-3 font-monospace"><span>${finalValue[0]}</span><span>${finalValue[1]}</span></div>`; } else { const displayValue = typeof finalValue === 'object' ? JSON.stringify(finalValue) : finalValue; displayHTML = displayValue.toString(); Loading Loading
noctua/web/pages/macros/widgets.html +73 −0 Original line number Diff line number Diff line Loading @@ -327,3 +327,76 @@ {% macro monitor_val_template() %} <span class="val-node"></span> {% endmacro %} {# --- WIDGET: Telemetry Card with Dynamic Fields --- #} {% macro widget_telemetry_card(label, fields) %} {% set safe_id = label | replace(' ', '-') | lower %} <div class="col-md-6" id="container-{{ safe_id }}"> <div class="card h-100 border-secondary shadow-sm"> <div class="card-header bg-dark d-flex justify-content-between align-items-center"> <span class="text-capitalize fw-bold">{{ label }}</span> <small class="text-muted timer">just now</small> </div> <div class="card-body p-0"> <!-- Raw JSON pre-renderer --> <pre class="p-3 m-0 text-light bg-black d-none" id="data-{{ safe_id }}" style="font-size: 0.75rem; white-space: pre-wrap;"></pre> <div id="pretty-{{ safe_id }}" class="pretty-container"> <table class="table table-dark mb-0 table-sm align-middle" style="font-size: 0.9rem;"> <thead> <tr class="text-muted small border-bottom border-secondary"> <th class="ps-3" style="width: 35%;">parameter</th> <th style="width: 50%;">status</th> <th style="width: 15%;" class="text-center">err</th> </tr> </thead> <tbody> {% for field in fields %} <tr class="border-bottom border-secondary border-opacity-25"> <td class="ps-3 fw-semibold py-2">{{ field.label }}</td> <td class="py-2"> {% if field.subfields %} <!-- Nested Object: render subtable key-value --> <table class="table table-sm table-borderless m-0 p-0" style="background: transparent;"> <tbody> {% for sub in field.subfields %} <tr> <td class="text-muted p-0 pe-2 small" style="width: 35%;">{{ sub.label }}</td> <td class="p-0"> {% if sub.is_array2 %} <div class="d-flex gap-3 font-monospace"> <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}-0">N/A</span> <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}-1">N/A</span> </div> {% else %} <span data-status="{{ safe_id }}-{{ field.endpoint }}-{{ sub.key }}">N/A</span> {% endif %} </td> </tr> {% endfor %} </tbody> </table> {% else %} <!-- Flat Field --> {% if field.is_array2 %} <div class="d-flex gap-3 font-monospace"> <span data-status="{{ safe_id }}-{{ field.endpoint }}-0">N/A</span> <span data-status="{{ safe_id }}-{{ field.endpoint }}-1">N/A</span> </div> {% else %} <span class="badge bg-secondary" data-status="{{ safe_id }}-{{ field.endpoint }}">N/A</span> {% endif %} {% endif %} </td> <td class="text-center py-2"> <span class="badge bg-success" data-status="{{ safe_id }}-{{ field.endpoint }}-error">OK</span> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> {% endmacro %}
noctua/web/pages/status.html +6 −6 Original line number Diff line number Diff line Loading @@ -21,15 +21,15 @@ </div> </div> <!-- Ordered containers --> <!-- Dynamic monitor cards populated automatically from WebSocket payload --> <div id="status-container" class="row g-3"> {{ w.widget_monitor(label="dome", safe_id="dome") }} {{ w.widget_monitor(label="stage", safe_id="stage") }} {{ w.widget_monitor(label="camera", safe_id="camera") }} {{ w.widget_monitor(label="telescope", safe_id="telescope") }} {{ w.widget_monitor("dome", "dome") }} {{ w.widget_monitor("stage", "stage") }} {{ w.widget_monitor("camera", "camera") }} {{ w.widget_monitor("telescope", "telescope") }} </div> <!-- Blueprints for dynamic parts --> <!-- Blueprints for dynamic parts (Cloned by JS, no HTML strings in JS) --> <template id="table-blueprint">{{ w.monitor_table_template() }}</template> <template id="row-blueprint">{{ w.monitor_row_template() }}</template> <template id="subtable-blueprint">{{ w.monitor_subtable_template() }}</template> Loading
noctua/web/static/js/status-view.js +19 −19 Original line number Diff line number Diff line // status-view.js // Renders subsystem tables dynamically, splitting 2-element arrays into two lines and objects into structured sub-tables. // Dynamic telemetry update loop. Clones HTML blueprints and maps keys as labels. document.addEventListener('DOMContentLoaded', () => { const tableBlueprint = document.getElementById('table-blueprint'); Loading @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { const globalRawCheckbox = document.getElementById('global-raw'); const globalForceCheckbox = document.getElementById('global-force'); // Central state cache to remember previous telemetry values for change detection // Central state cache to remember previous telemetry values for change-detection pulsing const previousState = {}; Loading Loading @@ -72,18 +72,18 @@ document.addEventListener('DOMContentLoaded', () => { const showRaw = globalRawCheckbox ? globalRawCheckbox.checked : false; // Initialize state cache for the subsystem if missing if (!previousState[subsystem]) { previousState[subsystem] = {}; } // Render raw JSON display if (dataPre) { dataPre.textContent = JSON.stringify(data, null, 4); dataPre.classList.toggle('d-none', !showRaw); } // 1. One-time table DOM assembly (builds the structure once) // Initialize state cache for the subsystem if missing if (!previousState[subsystem]) { previousState[subsystem] = {}; } // 1. One-time table DOM assembly (only runs on first websocket message for this subsystem) if (prettyDiv && prettyDiv.children.length === 0 && tableBlueprint && rowBlueprint) { prettyDiv.classList.toggle('d-none', showRaw); Loading @@ -92,7 +92,9 @@ document.addEventListener('DOMContentLoaded', () => { Object.entries(data).forEach(([deviceKey, val]) => { const rowClone = rowBlueprint.content.cloneNode(true); rowClone.querySelector('.device-name').textContent = deviceKey.replace('_', ' '); // NO EXPLICIT LABELS: Automatically use the JSON key as label, replacing underscores rowClone.querySelector('.device-name').textContent = deviceKey.replace(/_/g, ' '); const statusTd = rowClone.querySelector('.device-status'); const errTd = rowClone.querySelector('.device-err'); Loading @@ -112,7 +114,8 @@ document.addEventListener('DOMContentLoaded', () => { Object.entries(responseValue).forEach(([k, v]) => { const subrowClone = subrowBlueprint.content.cloneNode(true); subrowClone.querySelector('.key-cell').textContent = k.replace('_', ' '); // Use sub-key as label subrowClone.querySelector('.key-cell').textContent = k.replace(/_/g, ' '); // Bind nested status attribute const valCell = subrowClone.querySelector('.val-cell'); Loading Loading @@ -149,13 +152,13 @@ document.addEventListener('DOMContentLoaded', () => { let finalValue = 'N/A'; // Special case: update error badge cell if (subProperty === 'error') { if (statusKey.endsWith('-error')) { const errors = endpointData.error; const hasErrors = errors && errors.length > 0; const displayValue = hasErrors ? 'ERR' : 'OK'; const prevValue = el.textContent; const hasChanged = prevValue !== '' && prevValue !== 'N/A' && prevValue !== displayValue; const hasChanged = prevValue !== '' && prevValue !== displayValue; if (hasChanged) { el.classList.add('pulse-update'); Loading @@ -164,11 +167,8 @@ document.addEventListener('DOMContentLoaded', () => { el.textContent = displayValue; el.className = `badge ${hasErrors ? 'bg-danger' : 'bg-success'}`; if (hasErrors) { el.setAttribute('title', errors.join(', ')); } else { el.removeAttribute('title'); } if (hasErrors) el.setAttribute('title', errors.join(', ')); else el.removeAttribute('title'); return; } Loading @@ -189,12 +189,12 @@ document.addEventListener('DOMContentLoaded', () => { finalValue = 'N/A'; } // Dynamic layout generation: if value is a 2-element array, split on two lines // Dynamic layout: if value is a 2-element array, show side-by-side with a gap let displayHTML = ''; const isArray2 = Array.isArray(finalValue) && finalValue.length === 2; if (isArray2) { displayHTML = `<div class="small fw-bold">${finalValue[0]}</div><div class="small fw-bold">${finalValue[1]}</div>`; displayHTML = `<div class="d-flex gap-3 font-monospace"><span>${finalValue[0]}</span><span>${finalValue[1]}</span></div>`; } else { const displayValue = typeof finalValue === 'object' ? JSON.stringify(finalValue) : finalValue; displayHTML = displayValue.toString(); Loading