Files
vna_system/vna_system/web_ui/static/js/modules/settings.js
2025-11-24 21:09:28 +03:00

801 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 { LaserManager } from './settings/laser-manager.js';
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
import { renderIcons } from './icons.js';
import {
createPlotlyPlot,
togglePlotlyFullscreen,
downloadPlotlyImage,
cleanupPlotly
} from './plotly-utils.js';
import { apiGet, buildUrl } from './api-client.js';
import { API, TIMING, NOTIFICATION_TYPES } from './constants.js';
const { SUCCESS, ERROR, WARNING } = NOTIFICATION_TYPES;
export class SettingsManager {
constructor(notifications, websocket, acquisition) {
this.notifications = notifications;
this.websocket = websocket;
this.acquisition = acquisition;
this.isInitialized = false;
this.elements = {};
this.headerElements = {};
this.debouncer = new Debouncer();
// Sub-managers
this.presetManager = new PresetManager(notifications);
this.calibrationManager = new CalibrationManager(notifications);
this.referenceManager = new ReferenceManager(notifications);
this.laserManager = new LaserManager(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, 'Ошибка настроек', 'Не удалось инициализировать модуль настроек');
}
}
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'),
deleteCalibrationBtn: document.getElementById('deleteCalibrationBtn'),
clearCalibrationBtn: document.getElementById('clearCalibrationBtn'),
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'),
previewReferenceBtn: document.getElementById('previewReferenceBtn'),
currentReferenceInfo: document.getElementById('currentReferenceInfo'),
currentReferenceName: document.getElementById('currentReferenceName'),
currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
currentReferenceDescription: document.getElementById('currentReferenceDescription'),
currentReferenceCalibration: document.getElementById('currentReferenceCalibration'),
// Laser controls
laserManualMode: document.getElementById('laserManualMode'),
laserTemp1: document.getElementById('laserTemp1'),
laserTemp2: document.getElementById('laserTemp2'),
laserCurrent1: document.getElementById('laserCurrent1'),
laserCurrent2: document.getElementById('laserCurrent2'),
laserMinCurrent1: document.getElementById('laserMinCurrent1'),
laserMaxCurrent1: document.getElementById('laserMaxCurrent1'),
laserDeltaCurrent1: document.getElementById('laserDeltaCurrent1'),
laserScanTemp1: document.getElementById('laserScanTemp1'),
laserScanTemp2: document.getElementById('laserScanTemp2'),
laserScanCurrent2: document.getElementById('laserScanCurrent2'),
laserDeltaTime: document.getElementById('laserDeltaTime'),
laserTau: document.getElementById('laserTau'),
laserStartBtn: document.getElementById('laserStartBtn'),
laserStopBtn: document.getElementById('laserStopBtn'),
// Status
presetCount: document.getElementById('presetCount'),
calibrationCount: document.getElementById('calibrationCount'),
referenceCount: document.getElementById('referenceCount'),
systemStatus: document.getElementById('systemStatus')
};
this.headerElements = {
preset: document.getElementById('headerPresetSummary'),
calibration: document.getElementById('headerCalibrationSummary'),
reference: document.getElementById('headerReferenceSummary')
};
}
initSubManagers() {
this.presetManager.init(this.elements);
this.calibrationManager.init(this.elements);
this.referenceManager.init(this.elements);
this.laserManager.init({
manualMode: this.elements.laserManualMode,
temp1: this.elements.laserTemp1,
temp2: this.elements.laserTemp2,
current1: this.elements.laserCurrent1,
current2: this.elements.laserCurrent2,
minCurrent1: this.elements.laserMinCurrent1,
maxCurrent1: this.elements.laserMaxCurrent1,
deltaCurrent1: this.elements.laserDeltaCurrent1,
scanTemp1: this.elements.laserScanTemp1,
scanTemp2: this.elements.laserScanTemp2,
scanCurrent2: this.elements.laserScanCurrent2,
deltaTime: this.elements.laserDeltaTime,
tau: this.elements.laserTau,
startBtn: this.elements.laserStartBtn,
stopBtn: this.elements.laserStopBtn
});
// Setup callbacks
this.presetManager.onPresetChanged = async () => {
const status = await this.loadStatus();
this.calibrationManager.reset();
await this.calibrationManager.loadWorkingCalibration();
await this.ensureCurrentPreset(status);
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
};
this.calibrationManager.onCalibrationSaved = async () => {
const status = await this.loadStatus();
await this.ensureCurrentPreset(status);
};
this.calibrationManager.onCalibrationSet = async () => {
const status = await this.loadStatus();
await this.ensureCurrentPreset(status);
};
this.referenceManager.onReferenceUpdated = (reference) => {
this.updateReferenceSummary(reference);
};
this.referenceManager.onShowPlots = (plotData) => {
this.showPlotsModal(plotData);
};
}
setupEventHandlers() {
this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
}
async loadInitialData() {
await this.presetManager.loadPresets();
const status = await this.loadStatus();
await this.calibrationManager.loadWorkingCalibration();
await this.ensureCurrentPreset(status);
}
async loadStatus() {
try {
const status = await apiGet(API.SETTINGS.STATUS);
await this.updateStatusDisplay(status);
return status;
} catch (e) {
console.error('Status load failed:', e);
if (this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Не подключено';
}
if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = '-';
}
this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы');
return null;
}
}
async fetchCurrentPreset() {
try {
return await apiGet(API.SETTINGS.PRESET_CURRENT);
} catch (e) {
console.error('Current preset fetch failed:', e);
return null;
}
}
async ensureCurrentPreset(status) {
let preset = this.presetManager.getCurrentPreset();
if (preset) {
return preset;
}
const fallbackPreset = status?.current_preset ?? await this.fetchCurrentPreset();
if (!fallbackPreset) {
return null;
}
this.presetManager.setCurrentPresetDirect(fallbackPreset);
this.calibrationManager.setCurrentPreset(fallbackPreset);
await this.referenceManager.setCurrentPreset(fallbackPreset);
await this.calibrationManager.loadWorkingCalibration();
if (!status && this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Не подключено';
}
const effectiveStatus = {
...(status ?? { available_presets: 0, available_calibrations: 0, available_references: 0 }),
current_preset: fallbackPreset,
available_references: this.referenceManager?.availableReferences?.length ?? 0,
};
if (!status) {
if (this.elements.presetCount) {
this.elements.presetCount.textContent = effectiveStatus.available_presets ?? '-';
}
if (this.elements.calibrationCount) {
this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-';
}
if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = effectiveStatus.available_references ?? '-';
}
}
this.updateHeaderSummary(effectiveStatus);
return fallbackPreset;
}
async updateStatusDisplay(status) {
this.presetManager.updateStatus(status);
this.calibrationManager.updateStatus(status);
const preset = this.presetManager.getCurrentPreset();
this.calibrationManager.setCurrentPreset(preset);
await this.referenceManager.setCurrentPreset(preset);
if (this.elements.presetCount) {
this.elements.presetCount.textContent = status?.available_presets ?? '-';
}
if (this.elements.calibrationCount) {
this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-';
}
if (this.elements.referenceCount) {
const count = typeof status?.available_references === 'number'
? status.available_references
: this.referenceManager?.availableReferences?.length;
this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-';
}
this.updateHeaderSummary(status);
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
}
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: 'Загрузка...' });
const url = buildUrl(API.SETTINGS.CALIBRATION_STANDARDS_PLOTS(name), {
preset_filename: preset.filename
});
const plotsData = await apiGet(url);
this.showPlotsModal(plotsData);
} catch (e) {
console.error('Load plots failed:', e);
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики калибровки');
} finally {
ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Показать графики' });
}
}, TIMING.DEBOUNCE_CALIBRATION);
}
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: 'Загрузка...' });
const plotsData = await apiGet(API.SETTINGS.WORKING_CALIBRATION_PLOTS);
this.showPlotsModal(plotsData);
} catch (e) {
if (e.status === 404) {
this.notify(WARNING, 'Нет данных', 'Нет активной калибровки или стандартов для отображения');
} else {
console.error('Load current plots failed:', e);
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики текущей калибровки');
}
} finally {
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Графики текущей калибровки' });
}
}, TIMING.DEBOUNCE_CALIBRATION);
}
showPlotsModal(plotsData) {
const modal = this.elements.plotsModal;
if (!modal) return;
this.currentPlotsData = plotsData;
if (plotsData.reference_name) {
this.renderReferencePlot(plotsData);
} else {
this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
}
const title = modal.querySelector('.modal__title');
if (title) {
if (plotsData.reference_name) {
title.innerHTML = `
<span data-icon="target"></span>
${plotsData.reference_name} - ${plotsData.preset.mode.toUpperCase()}
`;
} else {
title.innerHTML = `
<span data-icon="bar-chart-3"></span>
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
`;
}
renderIcons(title);
}
this.setupModalCloseHandlers(modal);
modal.classList.add('modal--active');
document.body.style.overflow = 'hidden';
}
renderReferencePlot(plotsData) {
const container = this.elements.plotsGrid;
if (!container) return;
container.innerHTML = '';
if (!plotsData.plot || plotsData.plot.error) {
container.innerHTML = '<div class="plot-error">Не удалось загрузить график эталона</div>';
return;
}
const card = document.createElement('div');
card.className = 'chart-card';
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<span data-icon="target" class="chart-card__icon"></span>
${plotsData.reference_name}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
<span data-icon="expand"></span>
</button>
<button class="chart-card__action" data-action="download" title="Скачать">
<span data-icon="download"></span>
</button>
</div>
</div>
<div class="chart-card__content">
<div class="chart-card__plot" id="reference-plot"></div>
</div>
<div class="chart-card__meta">
<div class="chart-card__timestamp">Дата: ${new Date(plotsData.timestamp).toLocaleString()}</div>
<div class="chart-card__sweep">${plotsData.description || 'Эталон открытого пространства'}</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.downloadReferencePlot(plotsData, plotEl);
});
container.appendChild(card);
renderIcons(card);
const plotEl = card.querySelector('.chart-card__plot');
this.renderPlotly(plotEl, plotsData.plot, plotsData.reference_name);
}
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">Нет доступных графиков калибровки</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">
<span data-icon="alert-circle" class="chart-card__icon"></span>
${name.toUpperCase()} Standard
</div>
</div>
<div class="chart-card__content">
<div class="plot-error">Error: ${plot.error}</div>
</div>
`;
renderIcons(err);
container.appendChild(err);
return;
}
const card = this.createCalibrationChartCard(name, plot, preset);
container.appendChild(card);
});
renderIcons(container);
}
createCalibrationChartCard(standardName, plotConfig, preset) {
const card = document.createElement('div');
card.className = 'chart-card';
card.dataset.standard = standardName;
const title = `Стандарт ${standardName.toUpperCase()}`;
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<span data-icon="bar-chart-3" class="chart-card__icon"></span>
${title}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
<span data-icon="expand"></span>
</button>
<button class="chart-card__action" data-action="download" title="Скачать">
<span data-icon="download"></span>
</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">Стандарт: ${standardName.toUpperCase()}</div>
<div class="chart-card__sweep">Пресет: ${preset?.filename || 'Неизвестно'}</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);
renderIcons(card);
return card;
}
renderPlotly(container, plotConfig, title) {
if (!container || !plotConfig || plotConfig.error) {
container.innerHTML = `<div class="plot-error">Не удалось загрузить график: ${plotConfig?.error || 'Неизвестная ошибка'}</div>`;
return;
}
const layoutOverrides = {
title: { text: title, font: { size: 16, color: '#f1f5f9' } },
xaxis: {
...plotConfig.layout?.xaxis,
gridcolor: '#334155',
zerolinecolor: '#475569',
color: '#cbd5e1',
fixedrange: false
},
yaxis: {
...plotConfig.layout?.yaxis,
gridcolor: '#334155',
zerolinecolor: '#475569',
color: '#cbd5e1',
fixedrange: false
}
};
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(), TIMING.DEBOUNCE_DOWNLOAD)
);
}
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 downloadReferencePlot(plotsData, plotContainer) {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const base = `${plotsData.reference_name}_${ts}`;
if (plotContainer) {
await downloadPlotlyImage(plotContainer, `${base}_plot`);
}
const data = {
reference_info: {
name: plotsData.reference_name,
timestamp: plotsData.timestamp,
description: plotsData.description,
preset: plotsData.preset
},
plot_data: plotsData.plot
};
downloadJSON(data, `${base}_data.json`);
} catch (e) {
console.error('Download reference failed:', e);
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные эталона');
}
}
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`);
} catch (e) {
console.error('Download standard failed:', e);
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные калибровки');
}
}
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: 'Скачивание...' });
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, 'Полное скачивание завершено', `Полный набор данных калибровки сохранён для ${calibrationName}`);
} catch (e) {
console.error('Download all failed:', e);
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать полный набор данных калибровки');
} finally {
const btn = this.elements.downloadAllBtn;
if (btn) ButtonState.set(btn, { state: 'normal', icon: 'download-cloud', text: 'Скачать всё' });
}
}
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');
}
formatFrequency(value) {
if (value === null || value === undefined) return null;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
const abs = Math.abs(numeric);
const units = [
{ divider: 1e9, suffix: 'ГГц' },
{ divider: 1e6, suffix: 'МГц' },
{ divider: 1e3, suffix: 'кГц' }
];
for (const unit of units) {
if (abs >= unit.divider) {
const scaled = numeric / unit.divider;
const formatted = scaled >= 10 ? scaled.toFixed(0) : scaled.toFixed(2);
return `${formatted} ${unit.suffix}`;
}
}
return `${numeric.toFixed(0)} Гц`;
}
formatPresetSummary(preset) {
if (!preset) return 'Не выбран';
const parts = [];
if (preset.mode) {
parts.push(preset.mode.toUpperCase());
}
const start = this.formatFrequency(preset.start_freq);
const stop = this.formatFrequency(preset.stop_freq);
if (start && stop) {
parts.push(`${start} ${stop}`);
} else if (start || stop) {
parts.push(start || stop);
}
if (preset.points) {
parts.push(`${preset.points} точек`);
}
if (preset.bandwidth) {
const bw = this.formatFrequency(preset.bandwidth);
if (bw) parts.push(`ПП ${bw}`);
}
return parts.join(' • ') || 'Пресет настроен';
}
formatCalibrationSummary(status) {
const active = status?.current_calibration;
if (active?.calibration_name) {
return `Активна • ${active.calibration_name}`;
}
const working = status?.working_calibration;
if (working?.progress) {
const missingCount = working.missing_standards?.length || 0;
const missingText = missingCount ? ` • не хватает ${missingCount} стандартов` : '';
return `В процессе • ${working.progress}${missingText}`;
}
return 'Не задано';
}
updateHeaderSummary(status) {
if (!this.headerElements) return;
const { preset, calibration } = this.headerElements;
if (preset) {
preset.textContent = this.formatPresetSummary(status?.current_preset);
}
if (calibration) {
calibration.textContent = this.formatCalibrationSummary(status);
}
}
updateReferenceSummary(reference) {
const target = this.headerElements?.reference;
if (!target) return;
if (!reference) {
target.textContent = 'Не снят';
return;
}
const name = reference.name || 'Эталон';
let timestampText = '';
if (reference.timestamp) {
const timestamp = new Date(reference.timestamp);
if (!Number.isNaN(timestamp.getTime())) {
timestampText = timestamp.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' });
}
}
const summaryParts = ['Активен', name];
if (timestampText) {
summaryParts.push(timestampText);
}
target.textContent = summaryParts.join(' • ');
}
notify(type, title, message) {
this.notifications?.show?.({ type, title, message });
}
}