470 lines
18 KiB
JavaScript
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 });
|
|
}
|
|
} |