refactoring step 3
This commit is contained in:
@ -3,15 +3,16 @@
|
|||||||
* Handles Plotly.js chart creation, updates, and management
|
* 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 {
|
export class ChartManager {
|
||||||
constructor(config, notifications) {
|
constructor(config, notifications) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.notifications = notifications;
|
this.notifications = notifications;
|
||||||
|
|
||||||
this.charts = new Map(); // id -> { element, plotContainer, isVisible }
|
this.charts = new Map();
|
||||||
this.chartData = new Map(); // id -> [{ timestamp, metadata, data, plotly_config }]
|
this.chartData = new Map();
|
||||||
this.disabledProcessors = new Set();
|
this.disabledProcessors = new Set();
|
||||||
|
|
||||||
this.chartsGrid = null;
|
this.chartsGrid = null;
|
||||||
@ -28,11 +29,7 @@ export class ChartManager {
|
|||||||
lastUpdateTime: null
|
lastUpdateTime: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounce timers for settings updates
|
this.settingsManager = new ChartSettingsManager();
|
||||||
this.settingDebounceTimers = {};
|
|
||||||
|
|
||||||
// Track last setting values to prevent feedback loops
|
|
||||||
this.lastSettingValues = {};
|
|
||||||
|
|
||||||
this.plotlyConfig = {
|
this.plotlyConfig = {
|
||||||
displayModeBar: true,
|
displayModeBar: true,
|
||||||
@ -48,7 +45,7 @@ export class ChartManager {
|
|||||||
paper_bgcolor: 'transparent',
|
paper_bgcolor: 'transparent',
|
||||||
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' },
|
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' },
|
||||||
colorway: ['#3b82f6','#22c55e','#eab308','#ef4444','#8b5cf6','#06b6d4','#f59e0b','#10b981'],
|
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,
|
showlegend: true,
|
||||||
legend: {
|
legend: {
|
||||||
orientation: 'v',
|
orientation: 'v',
|
||||||
@ -76,7 +73,6 @@ export class ChartManager {
|
|||||||
console.log('Chart Manager initialized');
|
console.log('Chart Manager initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** New input format - direct payload from WS: processor_result */
|
|
||||||
addResult(payload) {
|
addResult(payload) {
|
||||||
try {
|
try {
|
||||||
const { processor_id, timestamp, plotly_config, metadata, data } = payload;
|
const { processor_id, timestamp, plotly_config, metadata, data } = payload;
|
||||||
@ -86,13 +82,11 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.disabledProcessors.has(processor_id)) {
|
if (this.disabledProcessors.has(processor_id)) {
|
||||||
console.log(` Skipping disabled processor: ${processor_id}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store only the latest data (no history needed)
|
|
||||||
this.chartData.set(processor_id, {
|
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 || {},
|
metadata: metadata || {},
|
||||||
data: data || {},
|
data: data || {},
|
||||||
plotly_config: plotly_config || { data: [], layout: {} }
|
plotly_config: plotly_config || { data: [], layout: {} }
|
||||||
@ -111,7 +105,7 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createChart(processorId) {
|
createChart(processorId) {
|
||||||
console.log(` Creating chart for processor: ${processorId}`);
|
console.log(`Creating chart for processor: ${processorId}`);
|
||||||
const card = this.createChartCard(processorId);
|
const card = this.createChartCard(processorId);
|
||||||
this.chartsGrid.appendChild(card);
|
this.chartsGrid.appendChild(card);
|
||||||
|
|
||||||
@ -132,7 +126,7 @@ export class ChartManager {
|
|||||||
plotContainer._resizeObserver = ro;
|
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++;
|
this.performanceStats.chartsCreated++;
|
||||||
|
|
||||||
if (this.config.animation) {
|
if (this.config.animation) {
|
||||||
@ -144,7 +138,10 @@ export class ChartManager {
|
|||||||
if (this.isPaused) return;
|
if (this.isPaused) return;
|
||||||
|
|
||||||
const chart = this.charts.get(processorId);
|
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 {
|
try {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
@ -160,19 +157,19 @@ export class ChartManager {
|
|||||||
await Plotly.react(chart.plotContainer, plotlyConfig.data || [], updateLayout, this.plotlyConfig);
|
await Plotly.react(chart.plotContainer, plotlyConfig.data || [], updateLayout, this.plotlyConfig);
|
||||||
|
|
||||||
this.updateChartMetadata(processorId);
|
this.updateChartMetadata(processorId);
|
||||||
// Only update settings if chart is newly created or if parameters actually changed
|
|
||||||
if (!chart.settingsInitialized) {
|
if (!chart.settingsInitialized) {
|
||||||
this.updateChartSettings(processorId);
|
this.updateChartSettings(processorId);
|
||||||
chart.settingsInitialized = true;
|
chart.settingsInitialized = true;
|
||||||
} else {
|
} else {
|
||||||
// Check if settings need updating without forcing it
|
|
||||||
this.updateChartSettings(processorId);
|
this.updateChartSettings(processorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dt = performance.now() - start;
|
const dt = performance.now() - start;
|
||||||
this.updatePerformanceStats(dt);
|
this.updatePerformanceStats(dt);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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) {
|
while (this.updateQueue.size > 0 && !this.isPaused) {
|
||||||
const [id, fn] = this.updateQueue.entries().next().value;
|
const [id, fn] = this.updateQueue.entries().next().value;
|
||||||
this.updateQueue.delete(id);
|
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));
|
await new Promise(r => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
@ -229,13 +226,11 @@ export class ChartManager {
|
|||||||
</div>
|
</div>
|
||||||
<div class="chart-card__meta">
|
<div class="chart-card__meta">
|
||||||
<div class="chart-card__timestamp" data-timestamp="">Last update: --</div>
|
<div class="chart-card__timestamp" data-timestamp="">Last update: --</div>
|
||||||
<div class="chart-card__sweep" data-sweep="">Info: --</div>
|
<div class="chart-card__sweep" data-sweep=""></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.setupChartCardEvents(card, processorId);
|
this.setupChartCardEvents(card, processorId);
|
||||||
|
|
||||||
// Initialize settings immediately
|
|
||||||
this.updateChartSettings(processorId);
|
this.updateChartSettings(processorId);
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
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 chart = this.charts.get(processorId);
|
||||||
const latestData = this.chartData.get(processorId);
|
const latestData = this.chartData.get(processorId);
|
||||||
if (!chart || !latestData) return;
|
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) {
|
if (tsEl) {
|
||||||
const dt = latestData.timestamp instanceof Date ? latestData.timestamp : new Date();
|
const dt = latestData.timestamp instanceof Date ? latestData.timestamp : new Date();
|
||||||
tsEl.textContent = `Last update: ${dt.toLocaleTimeString()}`;
|
tsEl.textContent = `Last update: ${dt.toLocaleTimeString()}`;
|
||||||
tsEl.dataset.timestamp = dt.toISOString();
|
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) {
|
updateChartSettings(processorId) {
|
||||||
const chart = this.charts.get(processorId);
|
const chart = this.charts.get(processorId);
|
||||||
const settingsContainer = chart?.element?.querySelector('.chart-settings__controls');
|
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 latestData = this.chartData.get(processorId);
|
||||||
const uiParameters = latestData?.metadata?.ui_parameters;
|
|
||||||
|
|
||||||
// Store current parameters to avoid unnecessary updates
|
if (settingsContainer && latestData) {
|
||||||
if (!chart.lastUiParameters) chart.lastUiParameters = null;
|
this.settingsManager.updateSettings(processorId, settingsContainer, latestData);
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = '<div class="settings-empty">No settings available</div>';
|
|
||||||
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); }
|
toggleProcessor(id, enabled) { enabled ? this.showChart(id) : this.hideChart(id); }
|
||||||
|
|
||||||
showChart(id) {
|
showChart(id) {
|
||||||
const c = this.charts.get(id);
|
const c = this.charts.get(id);
|
||||||
if (c) {
|
if (c) {
|
||||||
@ -488,6 +287,7 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
this.updateEmptyStateVisibility();
|
this.updateEmptyStateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
hideChart(id) {
|
hideChart(id) {
|
||||||
const c = this.charts.get(id);
|
const c = this.charts.get(id);
|
||||||
if (c) { c.element.classList.add('chart-card--hidden'); c.isVisible = false; }
|
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 timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const baseFilename = `${id}_${timestamp}`;
|
const baseFilename = `${id}_${timestamp}`;
|
||||||
|
|
||||||
// Download image
|
|
||||||
await Plotly.downloadImage(c.plotContainer, {
|
await Plotly.downloadImage(c.plotContainer, {
|
||||||
format: 'png',
|
format: 'png',
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@ -531,8 +330,7 @@ export class ChartManager {
|
|||||||
filename: `${baseFilename}_plot`
|
filename: `${baseFilename}_plot`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare and download processor data
|
const processorData = this.prepareDownloadData(id);
|
||||||
const processorData = this.prepareProcessorDownloadData(id);
|
|
||||||
if (processorData) {
|
if (processorData) {
|
||||||
downloadJSON(processorData, `${baseFilename}_data.json`);
|
downloadJSON(processorData, `${baseFilename}_data.json`);
|
||||||
}
|
}
|
||||||
@ -552,7 +350,7 @@ export class ChartManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareProcessorDownloadData(processorId) {
|
prepareDownloadData(processorId) {
|
||||||
const chart = this.charts.get(processorId);
|
const chart = this.charts.get(processorId);
|
||||||
const latestData = this.chartData.get(processorId);
|
const latestData = this.chartData.get(processorId);
|
||||||
|
|
||||||
@ -562,89 +360,17 @@ export class ChartManager {
|
|||||||
processor_info: {
|
processor_info: {
|
||||||
processor_id: processorId,
|
processor_id: processorId,
|
||||||
processor_name: formatProcessorName(processorId),
|
processor_name: formatProcessorName(processorId),
|
||||||
download_timestamp: new Date().toISOString(),
|
download_timestamp: new Date().toISOString()
|
||||||
is_visible: chart.isVisible
|
|
||||||
},
|
},
|
||||||
current_data: {
|
current_data: {
|
||||||
data: safeClone(latestData.data),
|
data: safeClone(latestData.data),
|
||||||
metadata: safeClone(latestData.metadata),
|
metadata: safeClone(latestData.metadata),
|
||||||
timestamp: latestData.timestamp instanceof Date ? latestData.timestamp.toISOString() : latestData.timestamp,
|
timestamp: latestData.timestamp instanceof Date ? latestData.timestamp.toISOString() : latestData.timestamp,
|
||||||
plotly_config: safeClone(latestData.plotly_config)
|
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) {
|
toggleFullscreen(id) {
|
||||||
const c = this.charts.get(id);
|
const c = this.charts.get(id);
|
||||||
if (!c?.element) return;
|
if (!c?.element) return;
|
||||||
@ -668,6 +394,7 @@ export class ChartManager {
|
|||||||
hideEmptyState() {
|
hideEmptyState() {
|
||||||
if (this.emptyState) this.emptyState.classList.add('empty-state--hidden');
|
if (this.emptyState) this.emptyState.classList.add('empty-state--hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEmptyStateVisibility() {
|
updateEmptyStateVisibility() {
|
||||||
if (!this.emptyState) return;
|
if (!this.emptyState) return;
|
||||||
const hasVisible = Array.from(this.charts.values()).some(c => c.isVisible);
|
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'); }
|
pause() { this.isPaused = true; console.log('Chart updates paused'); }
|
||||||
resume() { this.isPaused = false; console.log('Chart updates resumed'); if (this.updateQueue.size) this.processUpdateQueue(); }
|
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); }
|
getDisabledProcessors() { return Array.from(this.disabledProcessors); }
|
||||||
|
|
||||||
getStats() {
|
getStats() {
|
||||||
return {
|
return {
|
||||||
...this.performanceStats,
|
...this.performanceStats,
|
||||||
@ -792,9 +427,10 @@ export class ChartManager {
|
|||||||
destroy() {
|
destroy() {
|
||||||
console.log('Cleaning up Chart Manager...');
|
console.log('Cleaning up Chart Manager...');
|
||||||
this.clearAll();
|
this.clearAll();
|
||||||
|
this.settingsManager.destroy();
|
||||||
this.updateQueue.clear();
|
this.updateQueue.clear();
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
this.isPaused = true;
|
this.isPaused = true;
|
||||||
console.log('Chart Manager cleanup complete');
|
console.log('Chart Manager cleanup complete');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
174
vna_system/web_ui/static/js/modules/charts/chart-settings.js
Normal file
174
vna_system/web_ui/static/js/modules/charts/chart-settings.js
Normal file
@ -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 = '<div class="settings-empty">No settings available</div>';
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@
|
|||||||
* Handles toast notifications and user feedback
|
* Handles toast notifications and user feedback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { escapeHtml } from './utils.js';
|
||||||
|
|
||||||
export class NotificationManager {
|
export class NotificationManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.container = null;
|
this.container = null;
|
||||||
@ -183,10 +185,10 @@ export class NotificationManager {
|
|||||||
const iconHtml = `<i data-lucide="${notification.config.icon}" class="notification__icon"></i>`;
|
const iconHtml = `<i data-lucide="${notification.config.icon}" class="notification__icon"></i>`;
|
||||||
|
|
||||||
const titleHtml = notification.title ?
|
const titleHtml = notification.title ?
|
||||||
`<div class="notification__title">${this.escapeHtml(notification.title)}</div>` : '';
|
`<div class="notification__title">${escapeHtml(notification.title)}</div>` : '';
|
||||||
|
|
||||||
const messageHtml = notification.message ?
|
const messageHtml = notification.message ?
|
||||||
`<div class="notification__message">${this.escapeHtml(notification.message)}</div>` : '';
|
`<div class="notification__message">${escapeHtml(notification.message)}</div>` : '';
|
||||||
|
|
||||||
const actionsHtml = notification.actions.length > 0 ?
|
const actionsHtml = notification.actions.length > 0 ?
|
||||||
this.createActionsHtml(notification.actions) : '';
|
this.createActionsHtml(notification.actions) : '';
|
||||||
@ -224,7 +226,7 @@ export class NotificationManager {
|
|||||||
return `
|
return `
|
||||||
<button class="${buttonClass}" data-action="custom" data-action-id="${action.id}">
|
<button class="${buttonClass}" data-action="custom" data-action-id="${action.id}">
|
||||||
${action.icon ? `<i data-lucide="${action.icon}"></i>` : ''}
|
${action.icon ? `<i data-lucide="${action.icon}"></i>` : ''}
|
||||||
${this.escapeHtml(action.label)}
|
${escapeHtml(action.label)}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@ -471,18 +473,6 @@ export class NotificationManager {
|
|||||||
return Math.max(...notifications.map(n => n.createdAt.getTime()));
|
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, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup
|
* Cleanup
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Calibration Manager
|
||||||
|
* Handles VNA calibration workflow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
|
||||||
|
|
||||||
|
export class CalibrationManager {
|
||||||
|
constructor(notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.workingCalibration = null;
|
||||||
|
this.currentCalibration = null;
|
||||||
|
this.disabledStandards = new Set();
|
||||||
|
this.elements = {};
|
||||||
|
this.debouncer = new Debouncer();
|
||||||
|
this.reqGuard = new RequestGuard();
|
||||||
|
this.currentPreset = null;
|
||||||
|
|
||||||
|
this.handleStartCalibration = this.handleStartCalibration.bind(this);
|
||||||
|
this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
|
||||||
|
this.handleSetCalibration = this.handleSetCalibration.bind(this);
|
||||||
|
this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
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.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
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.resetCaptureState();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPreset(preset) {
|
||||||
|
this.currentPreset = preset;
|
||||||
|
if (preset) {
|
||||||
|
this.elements.startCalibrationBtn.disabled = false;
|
||||||
|
this.loadCalibrations();
|
||||||
|
} else {
|
||||||
|
this.elements.startCalibrationBtn.disabled = true;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.updateWorking(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.populateDropdown(calibrations);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Calibrations load failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDropdown(calibrations) {
|
||||||
|
const dd = this.elements.calibrationDropdown;
|
||||||
|
dd.innerHTML = '';
|
||||||
|
|
||||||
|
if (!calibrations.length) {
|
||||||
|
dd.innerHTML = '<option value="">No calibrations available</option>';
|
||||||
|
dd.disabled = true;
|
||||||
|
this.elements.setCalibrationBtn.disabled = true;
|
||||||
|
this.elements.viewPlotsBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd.innerHTML = '<option value="">Select calibration...</option>';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWorking(working) {
|
||||||
|
this.workingCalibration = working;
|
||||||
|
if (working.active) {
|
||||||
|
this.showSteps(working);
|
||||||
|
} else {
|
||||||
|
this.hideSteps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSteps(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSteps() {
|
||||||
|
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.getStandardsForMode();
|
||||||
|
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 = `<i data-lucide="clock"></i> Capturing ${std.toUpperCase()}...`;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.title = 'Standard is currently being captured';
|
||||||
|
} else if (isCompleted) {
|
||||||
|
btn.classList.add('btn--success');
|
||||||
|
btn.innerHTML = `<i data-lucide="check"></i> ${std.toUpperCase()}`;
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.title = 'Click to recapture this standard';
|
||||||
|
} else if (isMissing) {
|
||||||
|
btn.classList.add('btn--primary');
|
||||||
|
btn.innerHTML = `<i data-lucide="radio"></i> 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.captureStandard(std));
|
||||||
|
container.appendChild(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof lucide !== 'undefined') {
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStandardsForMode() {
|
||||||
|
if (!this.currentPreset) return [];
|
||||||
|
if (this.currentPreset.mode === 's11') return ['open', 'short', 'load'];
|
||||||
|
if (this.currentPreset.mode === 's21') return ['through'];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCalibrationChange() {
|
||||||
|
const v = this.elements.calibrationDropdown.value;
|
||||||
|
this.elements.setCalibrationBtn.disabled = !v;
|
||||||
|
this.elements.viewPlotsBtn.disabled = !v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStartCalibration() {
|
||||||
|
if (!this.currentPreset) return;
|
||||||
|
|
||||||
|
this.debouncer.debounce('start-calibration', () =>
|
||||||
|
this.reqGuard.runExclusive('start-calibration', async () => {
|
||||||
|
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();
|
||||||
|
} 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' });
|
||||||
|
}
|
||||||
|
}), 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async captureStandard(standard) {
|
||||||
|
const key = `calibrate-${standard}`;
|
||||||
|
if (this.disabledStandards.has(standard)) return;
|
||||||
|
|
||||||
|
this.debouncer.debounce(key, () =>
|
||||||
|
this.reqGuard.runExclusive(key, async () => {
|
||||||
|
try {
|
||||||
|
this.disabledStandards.add(standard);
|
||||||
|
|
||||||
|
const btn = document.querySelector(`[data-standard="${standard}"]`);
|
||||||
|
ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Capturing...' });
|
||||||
|
|
||||||
|
this.notify('info', 'Capturing Standard', `Capturing ${standard.toUpperCase()} standard...`);
|
||||||
|
|
||||||
|
const r = await fetch('/api/v1/settings/calibration/add-standard', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ standard })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
const result = await r.json();
|
||||||
|
|
||||||
|
this.notify('success', 'Standard Captured', result.message);
|
||||||
|
|
||||||
|
this.resetCaptureState();
|
||||||
|
await this.loadWorkingCalibration();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Capture standard failed:', e);
|
||||||
|
this.notify('error', 'Calibration Error', 'Failed to capture calibration standard');
|
||||||
|
this.resetCaptureState(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);
|
||||||
|
|
||||||
|
this.hideSteps();
|
||||||
|
this.elements.calibrationNameInput.value = '';
|
||||||
|
|
||||||
|
await this.loadWorkingCalibration();
|
||||||
|
await this.loadCalibrations();
|
||||||
|
|
||||||
|
if (this.onCalibrationSaved) {
|
||||||
|
await this.onCalibrationSaved();
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
if (this.onCalibrationSet) {
|
||||||
|
await this.onCalibrationSet();
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(status) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCaptureState(standard = null) {
|
||||||
|
if (standard) this.disabledStandards.delete(standard);
|
||||||
|
else this.disabledStandards.clear();
|
||||||
|
|
||||||
|
if (this.workingCalibration) {
|
||||||
|
this.renderStandardButtons(this.workingCalibration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.workingCalibration = null;
|
||||||
|
this.hideSteps();
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkingCalibration() {
|
||||||
|
return this.workingCalibration;
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(type, title, message) {
|
||||||
|
this.notifications?.show?.({ type, title, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
132
vna_system/web_ui/static/js/modules/settings/preset-manager.js
Normal file
132
vna_system/web_ui/static/js/modules/settings/preset-manager.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Preset Manager
|
||||||
|
* Handles VNA configuration presets
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
|
||||||
|
|
||||||
|
export class PresetManager {
|
||||||
|
constructor(notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.currentPreset = null;
|
||||||
|
this.elements = {};
|
||||||
|
this.debouncer = new Debouncer();
|
||||||
|
this.reqGuard = new RequestGuard();
|
||||||
|
|
||||||
|
this.handlePresetChange = this.handlePresetChange.bind(this);
|
||||||
|
this.handleSetPreset = this.handleSetPreset.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
this.elements.presetDropdown?.addEventListener('change', this.handlePresetChange);
|
||||||
|
this.elements.setPresetBtn?.addEventListener('click', this.handleSetPreset);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.elements.presetDropdown?.removeEventListener('change', this.handlePresetChange);
|
||||||
|
this.elements.setPresetBtn?.removeEventListener('click', this.handleSetPreset);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.populateDropdown(presets);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Presets load failed:', e);
|
||||||
|
this.notify('error', 'Load Error', 'Failed to load configuration presets');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDropdown(presets) {
|
||||||
|
const dd = this.elements.presetDropdown;
|
||||||
|
dd.innerHTML = '';
|
||||||
|
|
||||||
|
if (!presets.length) {
|
||||||
|
dd.innerHTML = '<option value="">No presets available</option>';
|
||||||
|
dd.disabled = true;
|
||||||
|
this.elements.setPresetBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd.innerHTML = '<option value="">Select preset...</option>';
|
||||||
|
presets.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.filename;
|
||||||
|
opt.textContent = this.formatDisplay(p);
|
||||||
|
dd.appendChild(opt);
|
||||||
|
});
|
||||||
|
|
||||||
|
dd.disabled = false;
|
||||||
|
this.elements.setPresetBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDisplay(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePresetChange() {
|
||||||
|
const v = this.elements.presetDropdown.value;
|
||||||
|
this.elements.setPresetBtn.disabled = !v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSetPreset() {
|
||||||
|
const filename = this.elements.presetDropdown.value;
|
||||||
|
if (!filename) return;
|
||||||
|
|
||||||
|
this.debouncer.debounce('set-preset', () =>
|
||||||
|
this.reqGuard.runExclusive('set-preset', async () => {
|
||||||
|
try {
|
||||||
|
ButtonState.set(this.elements.setPresetBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.currentPreset = { filename };
|
||||||
|
|
||||||
|
if (this.onPresetChanged) {
|
||||||
|
await this.onPresetChanged();
|
||||||
|
}
|
||||||
|
} 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' });
|
||||||
|
}
|
||||||
|
}), 300
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(status) {
|
||||||
|
if (status.current_preset) {
|
||||||
|
this.currentPreset = status.current_preset;
|
||||||
|
this.elements.currentPreset.textContent = status.current_preset.filename;
|
||||||
|
} else {
|
||||||
|
this.currentPreset = null;
|
||||||
|
this.elements.currentPreset.textContent = 'None';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPreset() {
|
||||||
|
return this.currentPreset;
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(type, title, message) {
|
||||||
|
this.notifications?.show?.({ type, title, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* Reference Manager
|
||||||
|
* Handles VNA measurement references
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
|
||||||
|
|
||||||
|
export class ReferenceManager {
|
||||||
|
constructor(notifications) {
|
||||||
|
this.notifications = notifications;
|
||||||
|
this.availableReferences = [];
|
||||||
|
this.currentReference = null;
|
||||||
|
this.currentPreset = null;
|
||||||
|
this.elements = {};
|
||||||
|
this.debouncer = new Debouncer();
|
||||||
|
this.reqGuard = new RequestGuard();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(elements) {
|
||||||
|
this.elements = elements;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPreset(preset) {
|
||||||
|
this.currentPreset = preset;
|
||||||
|
if (preset) {
|
||||||
|
this.loadReferences();
|
||||||
|
} else {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadReferences() {
|
||||||
|
try {
|
||||||
|
if (!this.currentPreset) {
|
||||||
|
this.renderDropdown([]);
|
||||||
|
this.updateInfo(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencesResponse = await fetch(`/api/v1/settings/references?preset_filename=${encodeURIComponent(this.currentPreset.filename)}`);
|
||||||
|
if (!referencesResponse.ok) throw new Error(`HTTP ${referencesResponse.status}`);
|
||||||
|
this.availableReferences = await referencesResponse.json();
|
||||||
|
|
||||||
|
const currentResponse = await fetch(`/api/v1/settings/reference/current?preset_filename=${encodeURIComponent(this.currentPreset.filename)}`);
|
||||||
|
if (!currentResponse.ok) throw new Error(`HTTP ${currentResponse.status}`);
|
||||||
|
this.currentReference = await currentResponse.json();
|
||||||
|
|
||||||
|
this.renderDropdown(this.availableReferences);
|
||||||
|
this.updateInfo(this.currentReference);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load references:', error);
|
||||||
|
this.renderDropdown([]);
|
||||||
|
this.updateInfo(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDropdown(references) {
|
||||||
|
if (!this.elements.referenceDropdown) return;
|
||||||
|
|
||||||
|
this.elements.referenceDropdown.innerHTML = '';
|
||||||
|
|
||||||
|
if (references.length === 0) {
|
||||||
|
this.elements.referenceDropdown.innerHTML = '<option value="">No references available</option>';
|
||||||
|
this.elements.referenceDropdown.disabled = true;
|
||||||
|
this.elements.setReferenceBtn.disabled = true;
|
||||||
|
this.elements.clearReferenceBtn.disabled = true;
|
||||||
|
this.elements.deleteReferenceBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
this.elements.referenceDropdown.innerHTML = '<option value="">Select a reference...</option>';
|
||||||
|
|
||||||
|
references.forEach(ref => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = ref.name;
|
||||||
|
option.textContent = `${ref.name} (${new Date(ref.timestamp).toLocaleDateString()})`;
|
||||||
|
this.elements.referenceDropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.elements.referenceDropdown.disabled = false;
|
||||||
|
|
||||||
|
if (this.currentReference) {
|
||||||
|
this.elements.referenceDropdown.value = this.currentReference.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtons() {
|
||||||
|
const hasSelection = this.elements.referenceDropdown.value !== '';
|
||||||
|
const hasCurrent = this.currentReference !== null;
|
||||||
|
|
||||||
|
this.elements.setReferenceBtn.disabled = !hasSelection;
|
||||||
|
this.elements.deleteReferenceBtn.disabled = !hasSelection;
|
||||||
|
this.elements.clearReferenceBtn.disabled = !hasCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInfo(reference) {
|
||||||
|
if (!this.elements.currentReferenceInfo) return;
|
||||||
|
|
||||||
|
if (!reference) {
|
||||||
|
this.elements.currentReferenceInfo.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elements.currentReferenceInfo.style.display = 'block';
|
||||||
|
|
||||||
|
if (this.elements.currentReferenceName) {
|
||||||
|
this.elements.currentReferenceName.textContent = reference.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.elements.currentReferenceTimestamp) {
|
||||||
|
this.elements.currentReferenceTimestamp.textContent = new Date(reference.timestamp).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.elements.currentReferenceDescription) {
|
||||||
|
this.elements.currentReferenceDescription.textContent = reference.description || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateReference() {
|
||||||
|
const name = this.elements.referenceNameInput.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
this.notify('warning', 'Missing Name', 'Please enter a name for the reference');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = this.elements.referenceDescriptionInput.value.trim();
|
||||||
|
|
||||||
|
this.debouncer.debounce('create-reference', () =>
|
||||||
|
this.reqGuard.runExclusive('create-reference', async () => {
|
||||||
|
try {
|
||||||
|
ButtonState.set(this.elements.createReferenceBtn, { state: 'loading', icon: 'loader', text: 'Creating...' });
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/reference/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, description })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notify('success', 'Reference Created', result.message);
|
||||||
|
|
||||||
|
this.elements.referenceNameInput.value = '';
|
||||||
|
this.elements.referenceDescriptionInput.value = '';
|
||||||
|
|
||||||
|
await this.loadReferences();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create reference failed:', error);
|
||||||
|
this.notify('error', 'Reference Error', 'Failed to create reference');
|
||||||
|
} finally {
|
||||||
|
ButtonState.set(this.elements.createReferenceBtn, { state: 'normal', icon: 'target', text: 'Capture Reference' });
|
||||||
|
}
|
||||||
|
}), 500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReferenceChange() {
|
||||||
|
this.updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSetReference() {
|
||||||
|
const referenceName = this.elements.referenceDropdown.value;
|
||||||
|
if (!referenceName) return;
|
||||||
|
|
||||||
|
this.debouncer.debounce('set-reference', () =>
|
||||||
|
this.reqGuard.runExclusive('set-reference', async () => {
|
||||||
|
try {
|
||||||
|
ButtonState.set(this.elements.setReferenceBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/reference/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: referenceName })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notify('success', 'Reference Set', result.message);
|
||||||
|
|
||||||
|
await this.loadReferences();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set reference failed:', error);
|
||||||
|
this.notify('error', 'Reference Error', 'Failed to set reference');
|
||||||
|
} finally {
|
||||||
|
ButtonState.set(this.elements.setReferenceBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
|
||||||
|
}
|
||||||
|
}), 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleClearReference() {
|
||||||
|
this.debouncer.debounce('clear-reference', () =>
|
||||||
|
this.reqGuard.runExclusive('clear-reference', async () => {
|
||||||
|
try {
|
||||||
|
ButtonState.set(this.elements.clearReferenceBtn, { state: 'loading', icon: 'loader', text: 'Clearing...' });
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/settings/reference/current', {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notify('success', 'Reference Cleared', result.message);
|
||||||
|
|
||||||
|
await this.loadReferences();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Clear reference failed:', error);
|
||||||
|
this.notify('error', 'Reference Error', 'Failed to clear reference');
|
||||||
|
} finally {
|
||||||
|
ButtonState.set(this.elements.clearReferenceBtn, { state: 'normal', icon: 'x', text: 'Clear' });
|
||||||
|
}
|
||||||
|
}), 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteReference() {
|
||||||
|
const referenceName = this.elements.referenceDropdown.value;
|
||||||
|
if (!referenceName) return;
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to delete reference "${referenceName}"? This action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debouncer.debounce('delete-reference', () =>
|
||||||
|
this.reqGuard.runExclusive('delete-reference', async () => {
|
||||||
|
try {
|
||||||
|
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'loading', icon: 'loader', text: 'Deleting...' });
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/settings/reference/${encodeURIComponent(referenceName)}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.notify('success', 'Reference Deleted', result.message);
|
||||||
|
|
||||||
|
await this.loadReferences();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete reference failed:', error);
|
||||||
|
this.notify('error', 'Reference Error', 'Failed to delete reference');
|
||||||
|
} finally {
|
||||||
|
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'normal', icon: 'trash-2', text: 'Delete' });
|
||||||
|
}
|
||||||
|
}), 400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.availableReferences = [];
|
||||||
|
this.currentReference = null;
|
||||||
|
this.renderDropdown([]);
|
||||||
|
this.updateInfo(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(type, title, message) {
|
||||||
|
this.notifications?.show?.({ type, title, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@
|
|||||||
* Handles localStorage persistence and user preferences
|
* Handles localStorage persistence and user preferences
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { formatBytes } from './utils.js';
|
||||||
|
|
||||||
export class StorageManager {
|
export class StorageManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.prefix = 'vna_dashboard_';
|
this.prefix = 'vna_dashboard_';
|
||||||
@ -271,7 +273,7 @@ export class StorageManager {
|
|||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
totalSize,
|
totalSize,
|
||||||
totalSizeFormatted: this.formatBytes(totalSize),
|
totalSizeFormatted: formatBytes(totalSize),
|
||||||
itemSizes,
|
itemSizes,
|
||||||
itemCount: Object.keys(itemSizes).length,
|
itemCount: Object.keys(itemSizes).length,
|
||||||
storageQuotaMB: this.estimateStorageQuota()
|
storageQuotaMB: this.estimateStorageQuota()
|
||||||
@ -291,19 +293,6 @@ export class StorageManager {
|
|||||||
return 5; // MB
|
return 5; // MB
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format bytes to human readable format
|
|
||||||
*/
|
|
||||||
formatBytes(bytes) {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup old or unnecessary data
|
* Cleanup old or unnecessary data
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user