Files
vna_system/vna_system/web_ui/static/js/modules/settings.js
2025-09-30 14:32:15 +03:00

470 lines
18 KiB
JavaScript

/**
* Settings Manager Module
* Coordinates preset, calibration, and reference management
*/
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';
import {
createPlotlyPlot,
togglePlotlyFullscreen,
downloadPlotlyImage,
cleanupPlotly
} from './plotly-utils.js';
export class SettingsManager {
constructor(notifications, websocket, acquisition) {
this.notifications = notifications;
this.websocket = websocket;
this.acquisition = acquisition;
this.isInitialized = false;
this.elements = {};
this.debouncer = new Debouncer();
// Sub-managers
this.presetManager = new PresetManager(notifications);
this.calibrationManager = new CalibrationManager(notifications);
this.referenceManager = new ReferenceManager(notifications);
// Plots modal state
this.currentPlotsData = null;
// Bind handlers
this.handleViewPlots = this.handleViewPlots.bind(this);
this.handleViewCurrentPlots = this.handleViewCurrentPlots.bind(this);
}
async init() {
try {
this.cacheDom();
this.initSubManagers();
this.setupEventHandlers();
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');
}
}
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')
};
}
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 {
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);
}
}
updateStatusDisplay(status) {
this.presetManager.updateStatus(status);
this.calibrationManager.updateStatus(status);
const preset = this.presetManager.getCurrentPreset();
this.calibrationManager.setCurrentPreset(preset);
this.referenceManager.setCurrentPreset(preset);
this.elements.presetCount.textContent = status.available_presets || 0;
this.elements.calibrationCount.textContent = status.available_calibrations || 0;
this.elements.systemStatus.textContent = 'Ready';
}
async handleViewPlots() {
this.debouncer.debounce('view-plots', async () => {
const name = this.elements.calibrationDropdown.value;
const preset = this.presetManager.getCurrentPreset();
if (!name || !preset) 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(preset.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', 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 = `
<i data-lucide="bar-chart-3"></i>
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
this.setupModalCloseHandlers(modal);
modal.classList.add('modal--active');
document.body.style.overflow = 'hidden';
}
renderCalibrationPlots(individualPlots, preset) {
const container = this.elements.plotsGrid;
if (!container) return;
container.innerHTML = '';
if (!individualPlots || !Object.keys(individualPlots).length) {
container.innerHTML = '<div class="plot-error">No calibration plots available</div>';
return;
}
Object.entries(individualPlots).forEach(([name, plot]) => {
if (plot.error) {
const err = document.createElement('div');
err.className = 'chart-card';
err.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="alert-circle" class="chart-card__icon"></i>
${name.toUpperCase()} Standard
</div>
</div>
<div class="chart-card__content">
<div class="plot-error">Error: ${plot.error}</div>
</div>
`;
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 } });
}
}
createCalibrationChartCard(standardName, plotConfig, preset) {
const card = document.createElement('div');
card.className = 'chart-card';
card.dataset.standard = standardName;
const title = `${standardName.toUpperCase()} Standard`;
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="bar-chart-3" class="chart-card__icon"></i>
${title}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
<i data-lucide="expand"></i>
</button>
<button class="chart-card__action" data-action="download" title="Download">
<i data-lucide="download"></i>
</button>
</div>
</div>
<div class="chart-card__content">
<div class="chart-card__plot" id="calibration-plot-${standardName}"></div>
</div>
<div class="chart-card__meta">
<div class="chart-card__timestamp">Standard: ${standardName.toUpperCase()}</div>
<div class="chart-card__sweep">Preset: ${preset?.filename || 'Unknown'}</div>
</div>
`;
card.addEventListener('click', (e) => {
const action = e.target.closest?.('[data-action]')?.dataset.action;
if (!action) return;
e.stopPropagation();
const plotEl = card.querySelector('.chart-card__plot');
if (action === 'fullscreen') this.toggleFullscreen(card);
if (action === 'download') this.downloadCalibrationStandard(standardName, plotEl);
});
const plotEl = card.querySelector('.chart-card__plot');
this.renderPlotly(plotEl, plotConfig, title);
return card;
}
renderPlotly(container, plotConfig, title) {
if (!container || !plotConfig || plotConfig.error) {
container.innerHTML = `<div class="plot-error">Failed to load plot: ${plotConfig?.error || 'Unknown error'}</div>`;
return;
}
const layoutOverrides = {
...plotConfig.layout,
title: { text: title, font: { size: 16, color: '#f1f5f9' } }
};
const configOverrides = {
toImageButtonOptions: {
format: 'png',
filename: `calibration-plot-${Date.now()}`,
height: 600,
width: 800,
scale: 1
}
};
createPlotlyPlot(container, plotConfig.data, layoutOverrides, configOverrides);
}
async toggleFullscreen(card) {
const plot = card.querySelector('.chart-card__plot');
await togglePlotlyFullscreen(card, plot);
}
setupModalCloseHandlers(modal) {
modal.querySelectorAll('[data-modal-close]').forEach(el => {
el.addEventListener('click', () => this.closePlotsModal());
});
const downloadAllBtn = modal.querySelector('#downloadAllBtn');
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', () =>
this.debouncer.debounce('download-all', () => this.downloadAllCalibrationData(), 600)
);
}
const escHandler = (e) => {
if (e.key === 'Escape') {
this.closePlotsModal();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
closePlotsModal() {
const modal = this.elements.plotsModal;
if (!modal) return;
modal.classList.remove('modal--active');
document.body.style.overflow = '';
const containers = modal.querySelectorAll('[id^="calibration-plot-"]');
containers.forEach(c => cleanupPlotly(c));
this.currentPlotsData = null;
}
async downloadCalibrationStandard(standardName, plotContainer) {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const calibrationName = this.currentPlotsData?.calibration_name || 'unknown';
const base = `${calibrationName}_${standardName}_${ts}`;
if (plotContainer) {
await downloadPlotlyImage(plotContainer, `${base}_plot`);
}
const data = this.prepareCalibrationDownloadData(standardName);
downloadJSON(data, `${base}_data.json`);
this.notify('success', 'Download Complete', `Downloaded ${standardName.toUpperCase()} standard plot and data`);
} catch (e) {
console.error('Download standard failed:', e);
this.notify('error', 'Download Failed', 'Failed to download calibration data');
}
}
async downloadAllCalibrationData() {
if (!this.currentPlotsData) return;
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const calibrationName = this.currentPlotsData.calibration_name || 'unknown';
const base = `${calibrationName}_complete_${ts}`;
const btn = this.elements.downloadAllBtn;
if (btn) ButtonState.set(btn, { state: 'loading', icon: 'loader', text: 'Downloading...' });
const complete = {
export_info: {
export_timestamp: new Date().toISOString(),
calibration_name: calibrationName,
preset: this.currentPlotsData.preset
},
standards_data: this.currentPlotsData.individual_plots
};
downloadJSON(complete, `${base}.json`);
this.notify('success', 'Complete Download', `Downloaded complete calibration data for ${calibrationName}`);
} catch (e) {
console.error('Download all failed:', e);
this.notify('error', 'Download Failed', 'Failed to download complete calibration data');
} finally {
const btn = this.elements.downloadAllBtn;
if (btn) ButtonState.set(btn, { state: 'normal', icon: 'download-cloud', text: 'Download All' });
}
}
prepareCalibrationDownloadData(standardName) {
if (!this.currentPlotsData) return null;
const plot = this.currentPlotsData.individual_plots[standardName];
return {
calibration_info: {
calibration_name: this.currentPlotsData.calibration_name,
preset: this.currentPlotsData.preset,
standard_name: standardName,
download_timestamp: new Date().toISOString()
},
plot_data: plot
};
}
async refresh() {
if (!this.isInitialized) return;
await this.loadInitialData();
}
destroy() {
this.presetManager.destroy();
this.calibrationManager.destroy();
this.referenceManager.destroy();
this.elements.viewPlotsBtn?.removeEventListener('click', this.handleViewPlots);
this.elements.viewCurrentPlotsBtn?.removeEventListener('click', this.handleViewCurrentPlots);
this.isInitialized = false;
console.log('Settings Manager destroyed');
}
notify(type, title, message) {
this.notifications?.show?.({ type, title, message });
}
}