diff --git a/vna_system/web_ui/static/js/modules/charts.js b/vna_system/web_ui/static/js/modules/charts.js
index 9c8f236..fe4c55d 100644
--- a/vna_system/web_ui/static/js/modules/charts.js
+++ b/vna_system/web_ui/static/js/modules/charts.js
@@ -3,15 +3,16 @@
* Handles Plotly.js chart creation, updates, and management
*/
-import { formatProcessorName, createParameterControl, safeClone, downloadJSON } from './utils.js';
+import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
+import { ChartSettingsManager } from './charts/chart-settings.js';
export class ChartManager {
constructor(config, notifications) {
this.config = config;
this.notifications = notifications;
- this.charts = new Map(); // id -> { element, plotContainer, isVisible }
- this.chartData = new Map(); // id -> [{ timestamp, metadata, data, plotly_config }]
+ this.charts = new Map();
+ this.chartData = new Map();
this.disabledProcessors = new Set();
this.chartsGrid = null;
@@ -28,11 +29,7 @@ export class ChartManager {
lastUpdateTime: null
};
- // Debounce timers for settings updates
- this.settingDebounceTimers = {};
-
- // Track last setting values to prevent feedback loops
- this.lastSettingValues = {};
+ this.settingsManager = new ChartSettingsManager();
this.plotlyConfig = {
displayModeBar: true,
@@ -48,7 +45,7 @@ export class ChartManager {
paper_bgcolor: 'transparent',
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' },
colorway: ['#3b82f6','#22c55e','#eab308','#ef4444','#8b5cf6','#06b6d4','#f59e0b','#10b981'],
- margin: { l: 60, r: 50, t: 50, b: 60 }, // Increased right and top margins
+ margin: { l: 60, r: 50, t: 50, b: 60 },
showlegend: true,
legend: {
orientation: 'v',
@@ -76,7 +73,6 @@ export class ChartManager {
console.log('Chart Manager initialized');
}
- /** New input format - direct payload from WS: processor_result */
addResult(payload) {
try {
const { processor_id, timestamp, plotly_config, metadata, data } = payload;
@@ -86,13 +82,11 @@ export class ChartManager {
}
if (this.disabledProcessors.has(processor_id)) {
- console.log(` Skipping disabled processor: ${processor_id}`);
return;
}
- // Store only the latest data (no history needed)
this.chartData.set(processor_id, {
- timestamp: new Date((timestamp ?? Date.now()) * 1000), // if timestamp is in epoch seconds
+ timestamp: new Date((timestamp ?? Date.now()) * 1000),
metadata: metadata || {},
data: data || {},
plotly_config: plotly_config || { data: [], layout: {} }
@@ -111,7 +105,7 @@ export class ChartManager {
}
createChart(processorId) {
- console.log(` Creating chart for processor: ${processorId}`);
+ console.log(`Creating chart for processor: ${processorId}`);
const card = this.createChartCard(processorId);
this.chartsGrid.appendChild(card);
@@ -132,7 +126,7 @@ export class ChartManager {
plotContainer._resizeObserver = ro;
}
- this.charts.set(processorId, { element: card, plotContainer, isVisible: true });
+ this.charts.set(processorId, { element: card, plotContainer, isVisible: true, settingsInitialized: false });
this.performanceStats.chartsCreated++;
if (this.config.animation) {
@@ -144,7 +138,10 @@ export class ChartManager {
if (this.isPaused) return;
const chart = this.charts.get(processorId);
- if (!chart?.plotContainer) { console.warn(` Chart not found for processor: ${processorId}`); return; }
+ if (!chart?.plotContainer) {
+ console.warn(`Chart not found for processor: ${processorId}`);
+ return;
+ }
try {
const start = performance.now();
@@ -160,19 +157,19 @@ export class ChartManager {
await Plotly.react(chart.plotContainer, plotlyConfig.data || [], updateLayout, this.plotlyConfig);
this.updateChartMetadata(processorId);
- // Only update settings if chart is newly created or if parameters actually changed
+
if (!chart.settingsInitialized) {
this.updateChartSettings(processorId);
chart.settingsInitialized = true;
} else {
- // Check if settings need updating without forcing it
this.updateChartSettings(processorId);
}
+
const dt = performance.now() - start;
this.updatePerformanceStats(dt);
});
} catch (e) {
- console.error(` Error updating chart ${processorId}:`, e);
+ console.error(`Error updating chart ${processorId}:`, e);
}
}
@@ -187,7 +184,7 @@ export class ChartManager {
while (this.updateQueue.size > 0 && !this.isPaused) {
const [id, fn] = this.updateQueue.entries().next().value;
this.updateQueue.delete(id);
- try { await fn(); } catch (e) { console.error(` Error in queued update for ${id}:`, e); }
+ try { await fn(); } catch (e) { console.error(`Error in queued update for ${id}:`, e); }
await new Promise(r => setTimeout(r, 0));
}
this.isUpdating = false;
@@ -229,13 +226,11 @@ export class ChartManager {
`;
this.setupChartCardEvents(card, processorId);
-
- // Initialize settings immediately
this.updateChartSettings(processorId);
if (typeof lucide !== 'undefined') lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
@@ -262,223 +257,27 @@ export class ChartManager {
const chart = this.charts.get(processorId);
const latestData = this.chartData.get(processorId);
if (!chart || !latestData) return;
- const tsEl = chart.element.querySelector('[data-timestamp]');
- const infoEl = chart.element.querySelector('[data-sweep]');
+ const tsEl = chart.element.querySelector('[data-timestamp]');
if (tsEl) {
const dt = latestData.timestamp instanceof Date ? latestData.timestamp : new Date();
tsEl.textContent = `Last update: ${dt.toLocaleTimeString()}`;
tsEl.dataset.timestamp = dt.toISOString();
}
-
- if (infoEl) {
- // Hide history count since we no longer track it
- infoEl.textContent = '';
- }
}
- /**
- * Update chart settings with current UI parameters
- */
updateChartSettings(processorId) {
const chart = this.charts.get(processorId);
const settingsContainer = chart?.element?.querySelector('.chart-settings__controls');
- if (!settingsContainer) return;
-
- // Get UI parameters from the latest chart data metadata
const latestData = this.chartData.get(processorId);
- const uiParameters = latestData?.metadata?.ui_parameters;
- // Store current parameters to avoid unnecessary updates
- if (!chart.lastUiParameters) chart.lastUiParameters = null;
-
- // Check if parameters have actually changed
- const parametersChanged = !chart.lastUiParameters ||
- JSON.stringify(uiParameters) !== JSON.stringify(chart.lastUiParameters);
-
- if (!parametersChanged) {
- console.log(` No parameter changes for ${processorId}, skipping settings update`);
- return; // No need to update if parameters haven't changed
+ if (settingsContainer && latestData) {
+ this.settingsManager.updateSettings(processorId, settingsContainer, latestData);
}
-
- console.log(` Updating settings for ${processorId}:`, {
- old: chart.lastUiParameters,
- new: uiParameters
- });
-
- chart.lastUiParameters = uiParameters ? JSON.parse(JSON.stringify(uiParameters)) : null;
-
- // Fallback to UI manager if no data available
- if (!uiParameters || !Array.isArray(uiParameters) || uiParameters.length === 0) {
- const uiManager = window.vnaDashboard?.ui;
- const processor = uiManager?.processors?.get(processorId);
-
- if (processor?.uiParameters && Array.isArray(processor.uiParameters) && processor.uiParameters.length > 0) {
- const settingsHtml = processor.uiParameters.map(param =>
- createParameterControl(param, processorId, 'chart')
- ).join('');
- settingsContainer.innerHTML = settingsHtml;
- this.setupSettingsEvents(settingsContainer, processorId);
- return;
- }
-
- settingsContainer.innerHTML = 'No settings available
';
- return;
- }
-
- // Generate settings HTML from chart data
- const settingsHtml = uiParameters.map(param =>
- createParameterControl(param, processorId, 'chart')
- ).join('');
-
- settingsContainer.innerHTML = settingsHtml;
-
- // Add event listeners
- this.setupSettingsEvents(settingsContainer, processorId);
-
- // Initialize last values from current UI state to prevent immediate updates
- if (uiParameters) {
- uiParameters.forEach(param => {
- const settingKey = `${processorId}_${param.name}`;
- this.lastSettingValues[settingKey] = param.value;
- console.log(` Initialized setting value: ${settingKey} = ${param.value}`);
- });
- }
-
- if (typeof lucide !== 'undefined') {
- lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
- }
- }
-
- /**
- * Setup event listeners for settings
- */
- setupSettingsEvents(settingsContainer, processorId) {
- const onParamChange = (e) => {
- if (!e.target.closest('.chart-setting')) return;
- this.handleSettingChange(e, processorId);
- };
-
- const onButtonClick = (e) => {
- if (!e.target.classList.contains('chart-setting__button')) return;
- this.handleButtonClick(e, processorId);
- };
-
- settingsContainer.addEventListener('input', onParamChange);
- settingsContainer.addEventListener('change', onParamChange);
- settingsContainer.addEventListener('click', onButtonClick);
- }
-
- /**
- * Handle setting parameter change
- */
- handleSettingChange(event, processorId) {
- const settingElement = event.target.closest('.chart-setting');
- if (!settingElement) return;
-
- const paramName = settingElement.dataset.param;
- const input = event.target;
-
- let value;
- if (input.type === 'checkbox') {
- value = input.checked;
- } else if (input.type === 'range') {
- value = parseFloat(input.value);
- // Update display value
- const valueDisplay = settingElement.querySelector('.chart-setting__value');
- if (valueDisplay) valueDisplay.textContent = value;
- } else {
- value = input.value;
- }
-
- // Normalize boolean values to prevent comparison issues
- if (typeof value === 'string' && (value === 'true' || value === 'false')) {
- value = value === 'true';
- }
-
- console.log(` Chart setting changed: ${processorId}.${paramName} = ${value}`);
-
- // Store the current setting value to prevent loops
- if (!this.lastSettingValues) this.lastSettingValues = {};
- const settingKey = `${processorId}_${paramName}`;
-
- // Normalize saved value for comparison
- let lastValue = this.lastSettingValues[settingKey];
- if (typeof lastValue === 'string' && (lastValue === 'true' || lastValue === 'false')) {
- lastValue = lastValue === 'true';
- }
-
- // Check if this is the same value we just set to prevent feedback loop
- if (lastValue === value) {
- console.log(` Skipping duplicate setting: ${settingKey} = ${value} (was ${this.lastSettingValues[settingKey]})`);
- return;
- }
-
- this.lastSettingValues[settingKey] = value;
-
- // Debounce setting updates to prevent rapid firing
- const debounceKey = `${processorId}_${paramName}`;
- if (this.settingDebounceTimers) {
- clearTimeout(this.settingDebounceTimers[debounceKey]);
- } else {
- this.settingDebounceTimers = {};
- }
-
- this.settingDebounceTimers[debounceKey] = setTimeout(() => {
- console.log(` Sending setting update: ${processorId}.${paramName} = ${value}`);
- // Send update via WebSocket
- const websocket = window.vnaDashboard?.websocket;
- if (websocket && websocket.recalculate) {
- websocket.recalculate(processorId, { [paramName]: value });
- } else {
- console.warn('WebSocket not available for settings update');
- }
- delete this.settingDebounceTimers[debounceKey];
- }, 300); // 300ms delay to prevent rapid updates
- }
-
- /**
- * Handle button click in settings
- */
- handleButtonClick(event, processorId) {
- const button = event.target;
- const paramName = button.dataset.param;
-
- if (!paramName) {
- console.warn('Button missing param data:', button);
- return;
- }
-
- // Prevent multiple clicks while processing
- if (button.disabled) {
- console.log('Button already processing, ignoring click');
- return;
- }
-
- console.log(` Button clicked: ${processorId}.${paramName}`);
-
- // Temporarily disable button and show feedback
- const originalText = button.textContent;
- button.disabled = true;
- button.textContent = '...';
-
- // Send button action via WebSocket (set to true to trigger action) - only once
- const websocket = window.vnaDashboard?.websocket;
- if (websocket && websocket.recalculate) {
- console.log(` Sending button action: ${processorId}.${paramName} = true`);
- websocket.recalculate(processorId, { [paramName]: true });
- } else {
- console.warn('WebSocket not available for button action');
- }
-
- // Re-enable button after a short delay
- setTimeout(() => {
- button.disabled = false;
- button.textContent = originalText;
- }, 1000);
}
toggleProcessor(id, enabled) { enabled ? this.showChart(id) : this.hideChart(id); }
+
showChart(id) {
const c = this.charts.get(id);
if (c) {
@@ -488,6 +287,7 @@ export class ChartManager {
}
this.updateEmptyStateVisibility();
}
+
hideChart(id) {
const c = this.charts.get(id);
if (c) { c.element.classList.add('chart-card--hidden'); c.isVisible = false; }
@@ -523,7 +323,6 @@ export class ChartManager {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const baseFilename = `${id}_${timestamp}`;
- // Download image
await Plotly.downloadImage(c.plotContainer, {
format: 'png',
width: 1200,
@@ -531,8 +330,7 @@ export class ChartManager {
filename: `${baseFilename}_plot`
});
- // Prepare and download processor data
- const processorData = this.prepareProcessorDownloadData(id);
+ const processorData = this.prepareDownloadData(id);
if (processorData) {
downloadJSON(processorData, `${baseFilename}_data.json`);
}
@@ -552,7 +350,7 @@ export class ChartManager {
}
}
- prepareProcessorDownloadData(processorId) {
+ prepareDownloadData(processorId) {
const chart = this.charts.get(processorId);
const latestData = this.chartData.get(processorId);
@@ -562,89 +360,17 @@ export class ChartManager {
processor_info: {
processor_id: processorId,
processor_name: formatProcessorName(processorId),
- download_timestamp: new Date().toISOString(),
- is_visible: chart.isVisible
+ download_timestamp: new Date().toISOString()
},
current_data: {
data: safeClone(latestData.data),
metadata: safeClone(latestData.metadata),
timestamp: latestData.timestamp instanceof Date ? latestData.timestamp.toISOString() : latestData.timestamp,
plotly_config: safeClone(latestData.plotly_config)
- },
- plot_config: this.getCurrentPlotlyDataSafe(processorId),
- ui_parameters: safeClone(this.getProcessorSettings(processorId)),
- raw_sweep_data: this.extractProcessorRawData(latestData),
- metadata: {
- description: `VNA processor data export - ${formatProcessorName(processorId)}`,
- format_version: "1.0",
- exported_by: "VNA System Dashboard",
- export_type: "processor_data",
- contains: [
- "Current processor data and metadata",
- "Plot configuration (Plotly format)",
- "UI parameter settings",
- "Raw measurement data if available",
- "Processing results and statistics"
- ]
}
};
}
- extractProcessorRawData(latestData) {
- // Extract raw data from processor results
- if (!latestData || !latestData.data) return null;
-
- try {
- const rawData = {
- processor_results: this.safeStringify(latestData.data),
- metadata: this.safeStringify(latestData.metadata)
- };
-
- // If this is magnitude processor, extract frequency/magnitude data
- if (latestData.plotly_config && latestData.plotly_config.data) {
- const plotData = latestData.plotly_config.data[0];
- if (plotData && plotData.x && plotData.y) {
- rawData.processed_measurements = [];
- const maxPoints = Math.min(plotData.x.length, plotData.y.length, 1000); // Limit to 1000 points
- for (let i = 0; i < maxPoints; i++) {
- rawData.processed_measurements.push({
- point_index: i,
- x_value: plotData.x[i],
- y_value: plotData.y[i],
- x_unit: this.getAxisUnit(plotData, 'x'),
- y_unit: this.getAxisUnit(plotData, 'y')
- });
- }
- }
- }
-
- return rawData;
- } catch (error) {
- console.warn('Error extracting processor raw data:', error);
- return { error: 'Failed to extract raw data' };
- }
- }
-
- safeStringify(obj) {
- try {
- return JSON.parse(JSON.stringify(obj));
- } catch (e) {
- return safeClone(obj);
- }
- }
-
- getAxisUnit(plotData, axis) {
- // Try to determine units from plot data or layout
- if (axis === 'x') {
- // For frequency data, usually GHz
- return plotData.name && plotData.name.includes('Frequency') ? 'GHz' : 'unknown';
- } else if (axis === 'y') {
- // For magnitude data, usually dB
- return plotData.name && plotData.name.includes('Magnitude') ? 'dB' : 'unknown';
- }
- return 'unknown';
- }
-
toggleFullscreen(id) {
const c = this.charts.get(id);
if (!c?.element) return;
@@ -668,6 +394,7 @@ export class ChartManager {
hideEmptyState() {
if (this.emptyState) this.emptyState.classList.add('empty-state--hidden');
}
+
updateEmptyStateVisibility() {
if (!this.emptyState) return;
const hasVisible = Array.from(this.charts.values()).some(c => c.isVisible);
@@ -684,100 +411,8 @@ export class ChartManager {
pause() { this.isPaused = true; console.log('Chart updates paused'); }
resume() { this.isPaused = false; console.log('Chart updates resumed'); if (this.updateQueue.size) this.processUpdateQueue(); }
-
- /**
- * Get current Plotly data/layout for a processor
- */
- getCurrentPlotlyData(processorId) {
- const chart = this.charts.get(processorId);
- if (!chart?.plotContainer?._fullData || !chart?.plotContainer?._fullLayout) {
- return null;
- }
-
- try {
- return {
- data: chart.plotContainer._fullData,
- layout: {
- title: chart.plotContainer._fullLayout.title,
- xaxis: chart.plotContainer._fullLayout.xaxis,
- yaxis: chart.plotContainer._fullLayout.yaxis,
- showlegend: chart.plotContainer._fullLayout.showlegend,
- legend: chart.plotContainer._fullLayout.legend
- }
- };
- } catch (error) {
- console.warn(` Could not extract Plotly data for ${processorId}:`, error);
- return null;
- }
- }
-
- /**
- * Safe version of getCurrentPlotlyData that avoids circular references
- */
- getCurrentPlotlyDataSafe(processorId) {
- const chart = this.charts.get(processorId);
- if (!chart?.plotContainer?._fullData || !chart?.plotContainer?._fullLayout) {
- return null;
- }
-
- try {
- // Extract only essential plot data to avoid circular references
- const data = [];
- if (chart.plotContainer._fullData) {
- chart.plotContainer._fullData.forEach(trace => {
- data.push({
- x: trace.x ? Array.from(trace.x) : null,
- y: trace.y ? Array.from(trace.y) : null,
- type: trace.type,
- mode: trace.mode,
- name: trace.name,
- line: trace.line ? {
- color: trace.line.color,
- width: trace.line.width
- } : null,
- marker: trace.marker ? {
- color: trace.marker.color,
- size: trace.marker.size
- } : null
- });
- });
- }
-
- const layout = {};
- if (chart.plotContainer._fullLayout) {
- const fullLayout = chart.plotContainer._fullLayout;
- layout.title = typeof fullLayout.title === 'string' ? fullLayout.title : fullLayout.title?.text;
- layout.xaxis = {
- title: fullLayout.xaxis?.title,
- range: fullLayout.xaxis?.range
- };
- layout.yaxis = {
- title: fullLayout.yaxis?.title,
- range: fullLayout.yaxis?.range
- };
- layout.showlegend = fullLayout.showlegend;
- }
-
- return { data, layout };
- } catch (error) {
- console.warn(` Could not extract safe Plotly data for ${processorId}:`, error);
- return null;
- }
- }
-
- /**
- * Get current processor settings (UI parameters)
- */
- getProcessorSettings(processorId) {
- const latestData = this.chartData.get(processorId);
- if (!latestData) {
- return null;
- }
-
- return latestData.metadata?.ui_parameters || null;
- }
-
getDisabledProcessors() { return Array.from(this.disabledProcessors); }
+
getStats() {
return {
...this.performanceStats,
@@ -792,9 +427,10 @@ export class ChartManager {
destroy() {
console.log('Cleaning up Chart Manager...');
this.clearAll();
+ this.settingsManager.destroy();
this.updateQueue.clear();
this.isUpdating = false;
this.isPaused = true;
console.log('Chart Manager cleanup complete');
}
-}
+}
\ No newline at end of file
diff --git a/vna_system/web_ui/static/js/modules/charts/chart-settings.js b/vna_system/web_ui/static/js/modules/charts/chart-settings.js
new file mode 100644
index 0000000..be76c67
--- /dev/null
+++ b/vna_system/web_ui/static/js/modules/charts/chart-settings.js
@@ -0,0 +1,174 @@
+/**
+ * Chart Settings Manager
+ * Handles chart parameter controls and WebSocket communication
+ */
+
+import { createParameterControl } from '../utils.js';
+
+export class ChartSettingsManager {
+ constructor() {
+ this.lastSettingValues = {};
+ this.settingDebounceTimers = {};
+ this.lastUiParameters = new Map();
+ }
+
+ updateSettings(processorId, settingsContainer, latestData) {
+ if (!settingsContainer) return;
+
+ const uiParameters = latestData?.metadata?.ui_parameters;
+
+ // Check if parameters have changed
+ const lastParams = this.lastUiParameters.get(processorId);
+ const parametersChanged = !lastParams || JSON.stringify(uiParameters) !== JSON.stringify(lastParams);
+
+ if (!parametersChanged) {
+ return;
+ }
+
+ console.log(`Updating settings for ${processorId}`);
+ this.lastUiParameters.set(processorId, uiParameters ? JSON.parse(JSON.stringify(uiParameters)) : null);
+
+ // Fallback to UI manager if no data available
+ if (!uiParameters || !Array.isArray(uiParameters) || uiParameters.length === 0) {
+ const uiManager = window.vnaDashboard?.ui;
+ const processor = uiManager?.processors?.get(processorId);
+
+ if (processor?.uiParameters && Array.isArray(processor.uiParameters) && processor.uiParameters.length > 0) {
+ const settingsHtml = processor.uiParameters.map(param =>
+ createParameterControl(param, processorId, 'chart')
+ ).join('');
+ settingsContainer.innerHTML = settingsHtml;
+ this.setupEvents(settingsContainer, processorId);
+ return;
+ }
+
+ settingsContainer.innerHTML = 'No settings available
';
+ return;
+ }
+
+ // Generate settings HTML
+ const settingsHtml = uiParameters.map(param =>
+ createParameterControl(param, processorId, 'chart')
+ ).join('');
+
+ settingsContainer.innerHTML = settingsHtml;
+ this.setupEvents(settingsContainer, processorId);
+
+ // Initialize last values
+ if (uiParameters) {
+ uiParameters.forEach(param => {
+ const settingKey = `${processorId}_${param.name}`;
+ this.lastSettingValues[settingKey] = param.value;
+ });
+ }
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
+ }
+ }
+
+ setupEvents(settingsContainer, processorId) {
+ const onParamChange = (e) => {
+ if (!e.target.closest('.chart-setting')) return;
+ this.handleSettingChange(e, processorId);
+ };
+
+ const onButtonClick = (e) => {
+ if (!e.target.classList.contains('chart-setting__button')) return;
+ this.handleButtonClick(e, processorId);
+ };
+
+ settingsContainer.addEventListener('input', onParamChange);
+ settingsContainer.addEventListener('change', onParamChange);
+ settingsContainer.addEventListener('click', onButtonClick);
+ }
+
+ handleSettingChange(event, processorId) {
+ const settingElement = event.target.closest('.chart-setting');
+ if (!settingElement) return;
+
+ const paramName = settingElement.dataset.param;
+ const input = event.target;
+
+ let value;
+ if (input.type === 'checkbox') {
+ value = input.checked;
+ } else if (input.type === 'range') {
+ value = parseFloat(input.value);
+ const valueDisplay = settingElement.querySelector('.chart-setting__value');
+ if (valueDisplay) valueDisplay.textContent = value;
+ } else {
+ value = input.value;
+ }
+
+ // Normalize boolean values
+ if (typeof value === 'string' && (value === 'true' || value === 'false')) {
+ value = value === 'true';
+ }
+
+ const settingKey = `${processorId}_${paramName}`;
+ let lastValue = this.lastSettingValues[settingKey];
+ if (typeof lastValue === 'string' && (lastValue === 'true' || lastValue === 'false')) {
+ lastValue = lastValue === 'true';
+ }
+
+ // Check for duplicate
+ if (lastValue === value) {
+ return;
+ }
+
+ this.lastSettingValues[settingKey] = value;
+
+ // Debounce updates
+ const debounceKey = `${processorId}_${paramName}`;
+ if (this.settingDebounceTimers[debounceKey]) {
+ clearTimeout(this.settingDebounceTimers[debounceKey]);
+ }
+
+ this.settingDebounceTimers[debounceKey] = setTimeout(() => {
+ console.log(`Sending setting update: ${processorId}.${paramName} = ${value}`);
+ const websocket = window.vnaDashboard?.websocket;
+ if (websocket && websocket.recalculate) {
+ websocket.recalculate(processorId, { [paramName]: value });
+ } else {
+ console.warn('WebSocket not available for settings update');
+ }
+ delete this.settingDebounceTimers[debounceKey];
+ }, 300);
+ }
+
+ handleButtonClick(event, processorId) {
+ const button = event.target;
+ const paramName = button.dataset.param;
+
+ if (!paramName || button.disabled) return;
+
+ console.log(`Button clicked: ${processorId}.${paramName}`);
+
+ const originalText = button.textContent;
+ button.disabled = true;
+ button.textContent = '...';
+
+ const websocket = window.vnaDashboard?.websocket;
+ if (websocket && websocket.recalculate) {
+ websocket.recalculate(processorId, { [paramName]: true });
+ } else {
+ console.warn('WebSocket not available for button action');
+ }
+
+ setTimeout(() => {
+ button.disabled = false;
+ button.textContent = originalText;
+ }, 1000);
+ }
+
+ destroy() {
+ // Clear timers
+ Object.keys(this.settingDebounceTimers).forEach(key => {
+ clearTimeout(this.settingDebounceTimers[key]);
+ });
+ this.settingDebounceTimers = {};
+ this.lastSettingValues = {};
+ this.lastUiParameters.clear();
+ }
+}
\ No newline at end of file
diff --git a/vna_system/web_ui/static/js/modules/notifications.js b/vna_system/web_ui/static/js/modules/notifications.js
index 6d4a5aa..70d1fc8 100644
--- a/vna_system/web_ui/static/js/modules/notifications.js
+++ b/vna_system/web_ui/static/js/modules/notifications.js
@@ -3,6 +3,8 @@
* Handles toast notifications and user feedback
*/
+import { escapeHtml } from './utils.js';
+
export class NotificationManager {
constructor() {
this.container = null;
@@ -183,10 +185,10 @@ export class NotificationManager {
const iconHtml = ``;
const titleHtml = notification.title ?
- `${this.escapeHtml(notification.title)}
` : '';
+ `${escapeHtml(notification.title)}
` : '';
const messageHtml = notification.message ?
- `${this.escapeHtml(notification.message)}
` : '';
+ `${escapeHtml(notification.message)}
` : '';
const actionsHtml = notification.actions.length > 0 ?
this.createActionsHtml(notification.actions) : '';
@@ -224,7 +226,7 @@ export class NotificationManager {
return `
`;
}).join('');
@@ -471,18 +473,6 @@ export class NotificationManager {
return Math.max(...notifications.map(n => n.createdAt.getTime()));
}
- /**
- * Escape HTML to prevent XSS
- */
- escapeHtml(unsafe) {
- return unsafe
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
-
/**
* Cleanup
*/
diff --git a/vna_system/web_ui/static/js/modules/settings.js b/vna_system/web_ui/static/js/modules/settings.js
index 55eec18..3e4c5ce 100644
--- a/vna_system/web_ui/static/js/modules/settings.js
+++ b/vna_system/web_ui/static/js/modules/settings.js
@@ -1,1372 +1,538 @@
/**
* Settings Manager Module
- * - Manage VNA presets
- * - Manage calibrations (working/current)
- * - Build calibration standard plots (Plotly)
- * - Protect from multiple requests: debounce + runExclusive (mutexes)
- * - Correct subscription/unsubscription to WebSocket events with the same handler
+ * Coordinates preset, calibration, and reference management
*/
-import { Debouncer, RequestGuard, ButtonState, downloadJSON } from './utils.js';
+import { PresetManager } from './settings/preset-manager.js';
+import { CalibrationManager } from './settings/calibration-manager.js';
+import { ReferenceManager } from './settings/reference-manager.js';
+import { Debouncer, ButtonState, downloadJSON } from './utils.js';
export class SettingsManager {
- /**
- * @param {object} notifications - Object with .show({type,title,message})
- * @param {object} websocket - Object with .on(event, handler) / .off(event, handler)
- * @param {object} acquisition - Object with .isRunning() / .triggerSingleSweep()
- */
- constructor(notifications, websocket, acquisition) {
- // Dependencies
- this.notifications = notifications;
- this.websocket = websocket;
- this.acquisition = acquisition;
+ constructor(notifications, websocket, acquisition) {
+ this.notifications = notifications;
+ this.websocket = websocket;
+ this.acquisition = acquisition;
- // State
- this.isInitialized = false;
- this.currentPreset = null;
- this.currentCalibration = null;
- this.workingCalibration = null;
- this.availableReferences = [];
- this.currentReference = null;
+ this.isInitialized = false;
+ this.elements = {};
+ this.debouncer = new Debouncer();
- // Calibration: capture state
- this.disabledStandards = new Set();
+ // Sub-managers
+ this.presetManager = new PresetManager(notifications);
+ this.calibrationManager = new CalibrationManager(notifications);
+ this.referenceManager = new ReferenceManager(notifications);
- // DOM cache
- this.elements = {};
+ // Plots modal state
+ this.currentPlotsData = null;
- // Guards
- this.debouncer = new Debouncer();
- this.reqGuard = new RequestGuard();
-
- // Bind UI handlers
- this.handlePresetChange = this.handlePresetChange.bind(this);
- this.handleSetPreset = this.handleSetPreset.bind(this);
- this.handleStartCalibration = this.handleStartCalibration.bind(this);
- this.handleCalibrateStandard = this.handleCalibrateStandard.bind(this);
- this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
- this.handleSetCalibration = this.handleSetCalibration.bind(this);
- this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
- this.handleViewPlots = this.handleViewPlots.bind(this);
- this.handleViewCurrentPlots = this.handleViewCurrentPlots.bind(this);
-
- // Reference handlers
- this.handleCreateReference = this.handleCreateReference.bind(this);
- this.handleReferenceChange = this.handleReferenceChange.bind(this);
- this.handleSetReference = this.handleSetReference.bind(this);
- this.handleClearReference = this.handleClearReference.bind(this);
- this.handleDeleteReference = this.handleDeleteReference.bind(this);
-
- // Data package for plots modal
- this.currentPlotsData = null;
- }
-
- /* ----------------------------- Lifecycle ----------------------------- */
-
- async init() {
- try {
- this._cacheDom();
- this._attachEvents();
-
- await this._loadInitialData();
-
- this.isInitialized = true;
- console.log('Settings Manager initialized');
- } catch (err) {
- console.error('Settings Manager init failed:', err);
- this._notify('error', 'Settings Error', 'Failed to initialize settings');
- }
- }
-
- destroy() {
- // Clean state and subscriptions
- this._resetCalibrationCaptureState();
- this._detachEvents();
- this.isInitialized = false;
- console.log('Settings Manager destroyed');
- }
-
- async refresh() {
- if (!this.isInitialized) return;
- await this._loadInitialData();
- }
-
- /* ----------------------------- DOM ----------------------------- */
-
- _cacheDom() {
- this.elements = {
- // Presets
- presetDropdown: document.getElementById('presetDropdown'),
- setPresetBtn: document.getElementById('setPresetBtn'),
- currentPreset: document.getElementById('currentPreset'),
-
- // Calibration
- currentCalibration: document.getElementById('currentCalibration'),
- startCalibrationBtn: document.getElementById('startCalibrationBtn'),
- calibrationSteps: document.getElementById('calibrationSteps'),
- calibrationStandards: document.getElementById('calibrationStandards'),
- progressText: document.getElementById('progressText'),
- calibrationNameInput: document.getElementById('calibrationNameInput'),
- saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
- calibrationDropdown: document.getElementById('calibrationDropdown'),
- setCalibrationBtn: document.getElementById('setCalibrationBtn'),
- viewPlotsBtn: document.getElementById('viewPlotsBtn'),
- viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
-
- // Modal
- plotsModal: document.getElementById('plotsModal'),
- plotsGrid: document.getElementById('plotsGrid'),
- downloadAllBtn: document.getElementById('downloadAllBtn'),
-
- // References
- referenceNameInput: document.getElementById('referenceNameInput'),
- referenceDescriptionInput: document.getElementById('referenceDescriptionInput'),
- createReferenceBtn: document.getElementById('createReferenceBtn'),
- referenceDropdown: document.getElementById('referenceDropdown'),
- setReferenceBtn: document.getElementById('setReferenceBtn'),
- clearReferenceBtn: document.getElementById('clearReferenceBtn'),
- deleteReferenceBtn: document.getElementById('deleteReferenceBtn'),
- currentReferenceInfo: document.getElementById('currentReferenceInfo'),
- currentReferenceName: document.getElementById('currentReferenceName'),
- currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
- currentReferenceDescription: document.getElementById('currentReferenceDescription'),
-
- // Status
- presetCount: document.getElementById('presetCount'),
- calibrationCount: document.getElementById('calibrationCount'),
- systemStatus: document.getElementById('systemStatus')
- };
- }
-
- _attachEvents() {
- // Presets
- this.elements.presetDropdown?.addEventListener('change', this.handlePresetChange);
- this.elements.setPresetBtn?.addEventListener('click', this.handleSetPreset);
-
- // Calibration
- this.elements.startCalibrationBtn?.addEventListener('click', this.handleStartCalibration);
- this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration);
- this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
- this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
- this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
- this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
-
- // References
- this.elements.createReferenceBtn?.addEventListener('click', this.handleCreateReference);
- this.elements.referenceDropdown?.addEventListener('change', this.handleReferenceChange);
- this.elements.setReferenceBtn?.addEventListener('click', this.handleSetReference);
- this.elements.clearReferenceBtn?.addEventListener('click', this.handleClearReference);
- this.elements.deleteReferenceBtn?.addEventListener('click', this.handleDeleteReference);
-
- // Name input → enables Save
- this.elements.calibrationNameInput?.addEventListener('input', () => {
- const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
- const isComplete = this.workingCalibration && this.workingCalibration.is_complete;
- this.elements.saveCalibrationBtn.disabled = !hasName || !isComplete;
- });
- }
-
- _detachEvents() {
- this.elements.presetDropdown?.removeEventListener('change', this.handlePresetChange);
- this.elements.setPresetBtn?.removeEventListener('click', this.handleSetPreset);
- this.elements.startCalibrationBtn?.removeEventListener('click', this.handleStartCalibration);
- this.elements.saveCalibrationBtn?.removeEventListener('click', this.handleSaveCalibration);
- this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange);
- this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration);
- this.elements.viewPlotsBtn?.removeEventListener('click', this.handleViewPlots);
- this.elements.viewCurrentPlotsBtn?.removeEventListener('click', this.handleViewCurrentPlots);
-
- // References
- this.elements.createReferenceBtn?.removeEventListener('click', this.handleCreateReference);
- this.elements.referenceDropdown?.removeEventListener('change', this.handleReferenceChange);
- this.elements.setReferenceBtn?.removeEventListener('click', this.handleSetReference);
- this.elements.clearReferenceBtn?.removeEventListener('click', this.handleClearReference);
- this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference);
-
- // WebSocket
- if (this.websocket) {
- this.websocket.off?.('processor_result', this._boundHandleSweepForCalibration);
- }
- }
-
- /* ----------------------------- Data Loading ----------------------------- */
-
- async _loadInitialData() {
- await Promise.all([
- this._loadPresets(),
- this._loadStatus(),
- this._loadWorkingCalibration(),
- this._loadReferences()
- ]);
- }
-
- async _loadPresets() {
- try {
- const r = await fetch('/api/v1/settings/presets');
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const presets = await r.json();
- this._populatePresetDropdown(presets);
- } catch (e) {
- console.error('Presets load failed:', e);
- this._notify('error', 'Load Error', 'Failed to load configuration presets');
- }
- }
-
- async _loadStatus() {
- try {
- const r = await fetch('/api/v1/settings/status');
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const status = await r.json();
- this._updateStatusDisplay(status);
- } catch (e) {
- console.error('Status load failed:', e);
- }
- }
-
- async _loadWorkingCalibration() {
- try {
- const r = await fetch('/api/v1/settings/working-calibration');
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const working = await r.json();
- this._updateWorkingCalibration(working);
- } catch (e) {
- console.error('Working calibration load failed:', e);
- }
- }
-
- async _loadCalibrations() {
- if (!this.currentPreset) return;
- try {
- const r = await fetch(`/api/v1/settings/calibrations?preset_filename=${encodeURIComponent(this.currentPreset.filename)}`);
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const calibrations = await r.json();
- this._populateCalibrationDropdown(calibrations);
- } catch (e) {
- console.error('Calibrations load failed:', e);
- }
- }
-
- /* ----------------------------- UI Populate ----------------------------- */
-
- _populatePresetDropdown(presets) {
- const dd = this.elements.presetDropdown;
- dd.innerHTML = '';
-
- if (!presets.length) {
- dd.innerHTML = '';
- dd.disabled = true;
- this.elements.setPresetBtn.disabled = true;
- return;
+ // Bind handlers
+ this.handleViewPlots = this.handleViewPlots.bind(this);
+ this.handleViewCurrentPlots = this.handleViewCurrentPlots.bind(this);
}
- dd.innerHTML = '';
- presets.forEach(p => {
- const opt = document.createElement('option');
- opt.value = p.filename;
- opt.textContent = this._formatPresetDisplay(p);
- dd.appendChild(opt);
- });
-
- dd.disabled = false;
- this.elements.setPresetBtn.disabled = true;
- }
-
- _populateCalibrationDropdown(calibrations) {
- const dd = this.elements.calibrationDropdown;
- dd.innerHTML = '';
-
- if (!calibrations.length) {
- dd.innerHTML = '';
- dd.disabled = true;
- this.elements.setCalibrationBtn.disabled = true;
- this.elements.viewPlotsBtn.disabled = true;
- return;
- }
-
- dd.innerHTML = '';
- calibrations.forEach(c => {
- const opt = document.createElement('option');
- opt.value = c.name;
- opt.textContent = `${c.name} ${c.is_complete ? '✔' : '?'}`;
- dd.appendChild(opt);
- });
-
- dd.disabled = false;
- this.elements.setCalibrationBtn.disabled = true;
- this.elements.viewPlotsBtn.disabled = true;
- }
-
- _formatPresetDisplay(p) {
- let s = `${p.filename} (${p.mode})`;
- if (p.start_freq && p.stop_freq) {
- const startMHz = (p.start_freq / 1e6).toFixed(0);
- const stopMHz = (p.stop_freq / 1e6).toFixed(0);
- s += ` - ${startMHz}-${stopMHz}MHz`;
- }
- if (p.points) s += `, ${p.points}pts`;
- return s;
- }
-
- _updateStatusDisplay(status) {
- // preset
- if (status.current_preset) {
- this.currentPreset = status.current_preset;
- this.elements.currentPreset.textContent = status.current_preset.filename;
- this.elements.startCalibrationBtn.disabled = false;
- this._loadCalibrations();
- } else {
- this.currentPreset = null;
- this.elements.currentPreset.textContent = 'None';
- this.elements.startCalibrationBtn.disabled = true;
- }
-
- // active calibration
- if (status.current_calibration) {
- this.currentCalibration = status.current_calibration;
- this.elements.currentCalibration.textContent = status.current_calibration.calibration_name;
- } else {
- this.currentCalibration = null;
- this.elements.currentCalibration.textContent = 'None';
- }
-
- // counts
- this.elements.presetCount.textContent = status.available_presets || 0;
- this.elements.calibrationCount.textContent = status.available_calibrations || 0;
- this.elements.systemStatus.textContent = 'Ready';
- }
-
- _updateWorkingCalibration(working) {
- this.workingCalibration = working;
- if (working.active) {
- this._showCalibrationSteps(working);
- } else {
- this._hideCalibrationSteps();
- }
- }
-
- _showCalibrationSteps(working) {
- this.elements.calibrationSteps.style.display = 'block';
- this.elements.progressText.textContent = working.progress || '0/0';
-
- this._renderStandardButtons(working);
-
- const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
- this.elements.saveCalibrationBtn.disabled = !hasName || !working.is_complete;
- this.elements.calibrationNameInput.disabled = false;
-
- const hasCompleted = (working.completed_standards || []).length > 0;
- if (this.elements.viewCurrentPlotsBtn) {
- this.elements.viewCurrentPlotsBtn.disabled = !hasCompleted;
- }
- }
-
- _hideCalibrationSteps() {
- this.elements.calibrationSteps.style.display = 'none';
- this.elements.calibrationStandards.innerHTML = '';
- if (this.elements.viewCurrentPlotsBtn) {
- this.elements.viewCurrentPlotsBtn.disabled = true;
- }
- }
-
- _renderStandardButtons(working) {
- const container = this.elements.calibrationStandards;
- container.innerHTML = '';
-
- const all = this._standardsForCurrentMode();
- const completed = working.completed_standards || [];
- const missing = working.missing_standards || [];
-
- all.forEach(std => {
- const btn = document.createElement('button');
- btn.className = 'btn calibration-standard-btn';
- btn.dataset.standard = std;
-
- const isCompleted = completed.includes(std);
- const isMissing = missing.includes(std);
- const capturing = this.disabledStandards.has(std);
-
- if (capturing) {
- btn.classList.add('btn--warning');
- btn.innerHTML = ` Capturing ${std.toUpperCase()}...`;
- btn.disabled = true;
- btn.title = 'Standard is currently being captured';
- } else if (isCompleted) {
- btn.classList.add('btn--success');
- btn.innerHTML = ` ${std.toUpperCase()}`;
- btn.disabled = false;
- btn.title = 'Click to recapture this standard';
- } else if (isMissing) {
- btn.classList.add('btn--primary');
- btn.innerHTML = ` Capture ${std.toUpperCase()}`;
- btn.disabled = false;
- btn.title = 'Click to capture this standard';
- } else {
- btn.classList.add('btn--secondary');
- btn.innerHTML = `${std.toUpperCase()}`;
- btn.disabled = true;
- }
-
- btn.addEventListener('click', () => this.handleCalibrateStandard(std));
- container.appendChild(btn);
- });
-
- if (typeof lucide !== 'undefined') {
- lucide.createIcons();
- }
- }
-
- _standardsForCurrentMode() {
- if (!this.currentPreset) return [];
- if (this.currentPreset.mode === 's11') return ['open', 'short', 'load'];
- if (this.currentPreset.mode === 's21') return ['through'];
- return [];
- }
-
- _resetCalibrationStateForPresetChange() {
- this.workingCalibration = null;
- this._hideCalibrationSteps();
- if (this.elements.calibrationNameInput) {
- this.elements.calibrationNameInput.value = '';
- this.elements.calibrationNameInput.disabled = true;
- }
- if (this.elements.saveCalibrationBtn) this.elements.saveCalibrationBtn.disabled = true;
- if (this.elements.progressText) this.elements.progressText.textContent = '0/0';
-
- // Reset reference state
- this.availableReferences = [];
- this.currentReference = null;
- console.log('Calibration and reference UI reset after preset change');
- }
-
- /* ----------------------------- Event Handlers (UI) ----------------------------- */
-
- handlePresetChange() {
- const v = this.elements.presetDropdown.value;
- this.elements.setPresetBtn.disabled = !v;
- }
-
- handleCalibrationChange() {
- const v = this.elements.calibrationDropdown.value;
- this.elements.setCalibrationBtn.disabled = !v;
- this.elements.viewPlotsBtn.disabled = !v;
- }
-
- async handleSetPreset() {
- const filename = this.elements.presetDropdown.value;
- if (!filename) return;
-
- this.debouncer.debounce('set-preset', () =>
- this.reqGuard.runExclusive('set-preset', async () => {
+ async init() {
try {
- ButtonState.set(this.elements.setPresetBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
+ this.cacheDom();
+ this.initSubManagers();
+ this.setupEventHandlers();
+ await this.loadInitialData();
- const r = await fetch('/api/v1/settings/preset/set', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ filename })
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const result = await r.json();
-
- this._notify('success', 'Preset Set', result.message);
-
- // Reset calibration UI
- this._resetCalibrationStateForPresetChange();
-
- // Update status
- await this._loadStatus();
- } catch (e) {
- console.error('Set preset failed:', e);
- this._notify('error', 'Preset Error', 'Failed to set configuration preset');
- } finally {
- ButtonState.set(this.elements.setPresetBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
+ this.isInitialized = true;
+ console.log('Settings Manager initialized');
+ } catch (err) {
+ console.error('Settings Manager init failed:', err);
+ this.notify('error', 'Settings Error', 'Failed to initialize settings');
}
- }), 300
- );
- }
+ }
- async handleStartCalibration() {
- if (!this.currentPreset) return;
+ cacheDom() {
+ this.elements = {
+ // Presets
+ presetDropdown: document.getElementById('presetDropdown'),
+ setPresetBtn: document.getElementById('setPresetBtn'),
+ currentPreset: document.getElementById('currentPreset'),
- this.debouncer.debounce('start-calibration', () =>
- this.reqGuard.runExclusive('start-calibration', async () => {
+ // Calibration
+ currentCalibration: document.getElementById('currentCalibration'),
+ startCalibrationBtn: document.getElementById('startCalibrationBtn'),
+ calibrationSteps: document.getElementById('calibrationSteps'),
+ calibrationStandards: document.getElementById('calibrationStandards'),
+ progressText: document.getElementById('progressText'),
+ calibrationNameInput: document.getElementById('calibrationNameInput'),
+ saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
+ calibrationDropdown: document.getElementById('calibrationDropdown'),
+ setCalibrationBtn: document.getElementById('setCalibrationBtn'),
+ viewPlotsBtn: document.getElementById('viewPlotsBtn'),
+ viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
+
+ // Modal
+ plotsModal: document.getElementById('plotsModal'),
+ plotsGrid: document.getElementById('plotsGrid'),
+ downloadAllBtn: document.getElementById('downloadAllBtn'),
+
+ // References
+ referenceNameInput: document.getElementById('referenceNameInput'),
+ referenceDescriptionInput: document.getElementById('referenceDescriptionInput'),
+ createReferenceBtn: document.getElementById('createReferenceBtn'),
+ referenceDropdown: document.getElementById('referenceDropdown'),
+ setReferenceBtn: document.getElementById('setReferenceBtn'),
+ clearReferenceBtn: document.getElementById('clearReferenceBtn'),
+ deleteReferenceBtn: document.getElementById('deleteReferenceBtn'),
+ currentReferenceInfo: document.getElementById('currentReferenceInfo'),
+ currentReferenceName: document.getElementById('currentReferenceName'),
+ currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
+ currentReferenceDescription: document.getElementById('currentReferenceDescription'),
+
+ // Status
+ presetCount: document.getElementById('presetCount'),
+ calibrationCount: document.getElementById('calibrationCount'),
+ systemStatus: document.getElementById('systemStatus')
+ };
+ }
+
+ initSubManagers() {
+ this.presetManager.init(this.elements);
+ this.calibrationManager.init(this.elements);
+ this.referenceManager.init(this.elements);
+
+ // Setup callbacks
+ this.presetManager.onPresetChanged = async () => {
+ await this.loadStatus();
+ const preset = this.presetManager.getCurrentPreset();
+ this.calibrationManager.setCurrentPreset(preset);
+ this.calibrationManager.reset();
+ this.referenceManager.setCurrentPreset(preset);
+ };
+
+ this.calibrationManager.onCalibrationSaved = async () => {
+ await this.loadStatus();
+ };
+
+ this.calibrationManager.onCalibrationSet = async () => {
+ await this.loadStatus();
+ };
+ }
+
+ setupEventHandlers() {
+ this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
+ this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
+ }
+
+ async loadInitialData() {
+ await Promise.all([
+ this.presetManager.loadPresets(),
+ this.loadStatus(),
+ this.calibrationManager.loadWorkingCalibration(),
+ this.referenceManager.loadReferences()
+ ]);
+ }
+
+ async loadStatus() {
try {
- ButtonState.set(this.elements.startCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Starting...' });
-
- const r = await fetch('/api/v1/settings/calibration/start', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ preset_filename: this.currentPreset.filename })
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const result = await r.json();
-
- this._notify('info', 'Calibration Started', `Started calibration for ${result.preset}`);
-
- await this._loadWorkingCalibration();
+ const r = await fetch('/api/v1/settings/status');
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ const status = await r.json();
+ this.updateStatusDisplay(status);
} catch (e) {
- console.error('Start calibration failed:', e);
- this._notify('error', 'Calibration Error', 'Failed to start calibration');
- } finally {
- ButtonState.set(this.elements.startCalibrationBtn, { state: 'normal', icon: 'play', text: 'Start Calibration' });
+ console.error('Status load failed:', e);
}
- }), 400
- );
- }
+ }
- async handleCalibrateStandard(standard) {
- const key = `calibrate-${standard}`;
- if (this.disabledStandards.has(standard)) return;
+ updateStatusDisplay(status) {
+ this.presetManager.updateStatus(status);
+ this.calibrationManager.updateStatus(status);
- this.debouncer.debounce(key, () =>
- this.reqGuard.runExclusive(key, async () => {
- try {
- // Mark standard as busy
- this.disabledStandards.add(standard);
+ const preset = this.presetManager.getCurrentPreset();
+ this.calibrationManager.setCurrentPreset(preset);
+ this.referenceManager.setCurrentPreset(preset);
- const btn = document.querySelector(`[data-standard="${standard}"]`);
- ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Capturing...' });
+ this.elements.presetCount.textContent = status.available_presets || 0;
+ this.elements.calibrationCount.textContent = status.available_calibrations || 0;
+ this.elements.systemStatus.textContent = 'Ready';
+ }
- this._notify('info', 'Capturing Standard', `Capturing ${standard.toUpperCase()} standard...`);
+ async handleViewPlots() {
+ this.debouncer.debounce('view-plots', async () => {
+ const name = this.elements.calibrationDropdown.value;
+ const preset = this.presetManager.getCurrentPreset();
+ if (!name || !preset) return;
- // Direct API call - backend will handle waiting and triggering
- const r = await fetch('/api/v1/settings/calibration/add-standard', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ standard })
- });
+ try {
+ ButtonState.set(this.elements.viewPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const result = await r.json();
+ const url = `/api/v1/settings/calibration/${encodeURIComponent(name)}/standards-plots?preset_filename=${encodeURIComponent(preset.filename)}`;
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ const plotsData = await r.json();
- this._notify('success', 'Standard Captured', result.message);
-
- // Reset state
- this._resetCalibrationCaptureState();
-
- // Reload working calibration
- await this._loadWorkingCalibration();
- } catch (e) {
- console.error('Capture standard failed:', e);
- this._notify('error', 'Calibration Error', 'Failed to capture calibration standard');
- this._resetCalibrationCaptureState(standard);
- }
- }), 500
- );
- }
-
- async handleSaveCalibration() {
- const name = this.elements.calibrationNameInput.value.trim();
- if (!name) return;
-
- this.debouncer.debounce('save-calibration', () =>
- this.reqGuard.runExclusive('save-calibration', async () => {
- try {
- ButtonState.set(this.elements.saveCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Saving...' });
-
- const r = await fetch('/api/v1/settings/calibration/save', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name })
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const result = await r.json();
-
- this._notify('success', 'Calibration Saved', result.message);
-
- // Clear working calibration in UI
- this._hideCalibrationSteps();
- this.elements.calibrationNameInput.value = '';
-
- await Promise.all([
- this._loadStatus(),
- this._loadWorkingCalibration(),
- this._loadCalibrations()
- ]);
- } catch (e) {
- console.error('Save calibration failed:', e);
- this._notify('error', 'Calibration Error', 'Failed to save calibration');
- } finally {
- ButtonState.set(this.elements.saveCalibrationBtn, { state: 'disabled', icon: 'save', text: 'Save Calibration' });
- }
- }), 400
- );
- }
-
- async handleSetCalibration() {
- this.debouncer.debounce('set-calibration', () =>
- this.reqGuard.runExclusive('set-calibration', async () => {
- const name = this.elements.calibrationDropdown.value;
- if (!name || !this.currentPreset) return;
-
- try {
- ButtonState.set(this.elements.setCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
-
- const r = await fetch('/api/v1/settings/calibration/set', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, preset_filename: this.currentPreset.filename })
- });
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const result = await r.json();
-
- this._notify('success', 'Calibration Set', result.message);
-
- await this._loadStatus();
- } catch (e) {
- console.error('Set calibration failed:', e);
- this._notify('error', 'Calibration Error', 'Failed to set active calibration');
- } finally {
- ButtonState.set(this.elements.setCalibrationBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
- }
- }), 300
- );
- }
-
- async handleViewPlots() {
- this.debouncer.debounce('view-plots', () =>
- this.reqGuard.runExclusive('view-plots', async () => {
- const name = this.elements.calibrationDropdown.value;
- if (!name || !this.currentPreset) return;
-
- try {
- ButtonState.set(this.elements.viewPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
-
- const url = `/api/v1/settings/calibration/${encodeURIComponent(name)}/standards-plots?preset_filename=${encodeURIComponent(this.currentPreset.filename)}`;
- const r = await fetch(url);
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- const plotsData = await r.json();
-
- this._showPlotsModal(plotsData);
- } catch (e) {
- console.error('Load plots failed:', e);
- this._notify('error', 'Plots Error', 'Failed to load calibration plots');
- } finally {
- ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Plots' });
- }
- }), 300
- );
- }
-
- async handleViewCurrentPlots() {
- this.debouncer.debounce('view-current-plots', () =>
- this.reqGuard.runExclusive('view-current-plots', async () => {
- if (!this.workingCalibration || !this.workingCalibration.active) return;
-
- try {
- ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
-
- const r = await fetch('/api/v1/settings/working-calibration/standards-plots');
- if (!r.ok) {
- if (r.status === 404) {
- this._notify('warning', 'No Data', 'No working calibration or standards available to plot');
- return;
+ this.showPlotsModal(plotsData);
+ } catch (e) {
+ console.error('Load plots failed:', e);
+ this.notify('error', 'Plots Error', 'Failed to load calibration plots');
+ } finally {
+ ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Plots' });
}
- throw new Error(`HTTP ${r.status}`);
- }
- const plotsData = await r.json();
- this._showPlotsModal(plotsData);
- } catch (e) {
- console.error('Load current plots failed:', e);
- this._notify('error', 'Plots Error', 'Failed to load current calibration plots');
- } finally {
- ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Current Plots' });
+ }, 300);
+ }
+
+ async handleViewCurrentPlots() {
+ this.debouncer.debounce('view-current-plots', async () => {
+ const working = this.calibrationManager.getWorkingCalibration();
+ if (!working || !working.active) return;
+
+ try {
+ ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
+
+ const r = await fetch('/api/v1/settings/working-calibration/standards-plots');
+ if (!r.ok) {
+ if (r.status === 404) {
+ this.notify('warning', 'No Data', 'No working calibration or standards available to plot');
+ return;
+ }
+ throw new Error(`HTTP ${r.status}`);
+ }
+ const plotsData = await r.json();
+ this.showPlotsModal(plotsData);
+ } catch (e) {
+ console.error('Load current plots failed:', e);
+ this.notify('error', 'Plots Error', 'Failed to load current calibration plots');
+ } finally {
+ ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Current Plots' });
+ }
+ }, 300);
+ }
+
+ showPlotsModal(plotsData) {
+ const modal = this.elements.plotsModal;
+ if (!modal) return;
+
+ this.currentPlotsData = plotsData;
+
+ this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
+
+ const title = modal.querySelector('.modal__title');
+ if (title) {
+ title.innerHTML = `
+
+ ${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
+ `;
+ if (typeof lucide !== 'undefined') lucide.createIcons();
}
- }), 300
- );
- }
- /* ----------------------------- WebSocket sweep capture ----------------------------- */
+ this.setupModalCloseHandlers(modal);
-
- _resetCalibrationCaptureState(standard = null) {
- if (standard) this.disabledStandards.delete(standard);
- else this.disabledStandards.clear();
-
- if (this.workingCalibration) {
- this._renderStandardButtons(this.workingCalibration);
- }
- }
-
-
- /* ----------------------------- Plots Modal ----------------------------- */
-
- _showPlotsModal(plotsData) {
- const modal = this.elements.plotsModal;
- if (!modal) return;
-
- // Store data package
- this.currentPlotsData = plotsData;
-
- // Render cards/plots
- this._renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
-
- // Header
- const title = modal.querySelector('.modal__title');
- if (title) {
- title.innerHTML = `
-
- ${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
- `;
- if (typeof lucide !== 'undefined') lucide.createIcons();
+ modal.classList.add('modal--active');
+ document.body.style.overflow = 'hidden';
}
- // Close/download buttons
- this._setupModalCloseHandlers(modal);
+ renderCalibrationPlots(individualPlots, preset) {
+ const container = this.elements.plotsGrid;
+ if (!container) return;
- // Display
- modal.classList.add('modal--active');
- document.body.style.overflow = 'hidden';
- }
+ container.innerHTML = '';
- _setupModalCloseHandlers(modal) {
- modal.querySelectorAll('[data-modal-close]').forEach(el => {
- el.addEventListener('click', () => this.closePlotsModal());
- });
+ if (!individualPlots || !Object.keys(individualPlots).length) {
+ container.innerHTML = 'No calibration plots available
';
+ return;
+ }
- const downloadAllBtn = modal.querySelector('#downloadAllBtn');
- if (downloadAllBtn) {
- downloadAllBtn.addEventListener('click', () =>
- this.debouncer.debounce('download-all', () => this.downloadAllCalibrationData(), 600)
- );
+ Object.entries(individualPlots).forEach(([name, plot]) => {
+ if (plot.error) {
+ const err = document.createElement('div');
+ err.className = 'chart-card';
+ err.innerHTML = `
+
+
+ `;
+ container.appendChild(err);
+ return;
+ }
+
+ const card = this.createCalibrationChartCard(name, plot, preset);
+ container.appendChild(card);
+ });
+
+ if (typeof lucide !== 'undefined') {
+ lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
+ }
}
- const escHandler = (e) => {
- if (e.key === 'Escape') {
- this.closePlotsModal();
- document.removeEventListener('keydown', escHandler);
- }
- };
- document.addEventListener('keydown', escHandler);
- }
+ createCalibrationChartCard(standardName, plotConfig, preset) {
+ const card = document.createElement('div');
+ card.className = 'chart-card';
+ card.dataset.standard = standardName;
- _renderCalibrationPlots(individualPlots, preset) {
- const container = this.elements.plotsGrid;
- if (!container) return;
+ const title = `${standardName.toUpperCase()} Standard`;
- container.innerHTML = '';
-
- if (!individualPlots || !Object.keys(individualPlots).length) {
- container.innerHTML = 'No calibration plots available
';
- return;
- }
-
- Object.entries(individualPlots).forEach(([name, plot]) => {
- if (plot.error) {
- const err = document.createElement('div');
- err.className = 'chart-card';
- err.innerHTML = `
-