updated view and added calibration deletion

This commit is contained in:
Ayzen
2025-09-30 18:09:50 +03:00
parent 7c26285e7f
commit facff2b4f8
23 changed files with 327 additions and 12183 deletions

View File

@ -621,3 +621,33 @@ async def delete_reference(reference_name: str, preset_filename: str | None = No
logger.error("Failed to delete reference", name=reference_name, error=repr(exc)) logger.error("Failed to delete reference", name=reference_name, error=repr(exc))
raise HTTPException(status_code=500, detail=str(exc)) raise HTTPException(status_code=500, detail=str(exc))
@router.delete("/calibration/{calibration_name}")
async def delete_calibration(calibration_name: str, preset_filename: str | None = None) -> dict[str, Any]:
"""Delete a calibration set."""
try:
settings_manager = singletons.settings_manager
if preset_filename:
preset = next(
(p for p in settings_manager.get_available_presets() if p.filename == preset_filename),
None
)
if not preset:
raise HTTPException(status_code=404, detail=f"Preset '{preset_filename}' not found")
else:
preset = settings_manager.get_current_preset()
if not preset:
raise HTTPException(status_code=400, detail="No current preset set and no preset_filename provided")
settings_manager.delete_calibration(preset, calibration_name)
return {
"success": True,
"message": f"Calibration '{calibration_name}' deleted"
}
except HTTPException:
raise
except Exception as exc: # noqa: BLE001
logger.error("Failed to delete calibration", name=calibration_name, error=repr(exc))
raise HTTPException(status_code=500, detail=str(exc))

View File

@ -1 +1 @@
config_inputs/s21_start100_stop8800_points1000_bw1khz.bin config_inputs/s11_start100_stop8800_points1000_bw1khz.bin

View File

@ -0,0 +1 @@
s11_start100_stop8800_points1000_bw1khz/еуыеуые

View File

@ -1,18 +0,0 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "яыф",
"standards": [
"open",
"load",
"short"
],
"created_timestamp": "2025-09-26T17:20:00.022650",
"is_complete": true
}

View File

@ -1,16 +0,0 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "яыф",
"standard": "load",
"sweep_number": 12,
"sweep_timestamp": 1758896376.33808,
"created_timestamp": "2025-09-26T17:20:00.020322",
"total_points": 1000
}

View File

@ -1,16 +0,0 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "яыф",
"standard": "open",
"sweep_number": 17,
"sweep_timestamp": 1758896395.4880857,
"created_timestamp": "2025-09-26T17:20:00.016886",
"total_points": 1000
}

View File

@ -1,16 +0,0 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "яыф",
"standard": "short",
"sweep_number": 13,
"sweep_timestamp": 1758896378.4093437,
"created_timestamp": "2025-09-26T17:20:00.022500",
"total_points": 1000
}

View File

@ -2,9 +2,9 @@
"open_air": true, "open_air": true,
"axis": "abs", "axis": "abs",
"data_limitation": "ph_only_1", "data_limitation": "ph_only_1",
"cut": 1.291, "cut": 2.0,
"max": 1.0, "max": 5.0,
"gain": 0.9, "gain": 2.2,
"start_freq": 100.0, "start_freq": 100.0,
"stop_freq": 8800.0, "stop_freq": 8800.0,
"clear_history": false, "clear_history": false,

View File

@ -1,5 +1,5 @@
{ {
"y_min": -80, "y_min": -80,
"y_max": 40, "y_max": 40,
"show_phase": true "show_phase": false
} }

View File

@ -183,6 +183,10 @@ class BScanProcessor(BaseProcessor):
latest_history = self._sweep_history[-1] latest_history = self._sweep_history[-1]
reference_data = latest_history.get("reference_data") reference_data = latest_history.get("reference_data")
if reference_data is None:
logger.warning(f"Open air substraction cannot be done: reference_data is None")
self._config["open_air"] = False
if self._config["open_air"] and reference_data is not None: if self._config["open_air"] and reference_data is not None:
reference_complex = self._get_complex_s11(reference_data) reference_complex = self._get_complex_s11(reference_data)
if reference_complex is not None and reference_complex.size: if reference_complex is not None and reference_complex.size:

View File

@ -188,6 +188,10 @@ class VNASettingsManager:
raise ValueError("No current preset available") raise ValueError("No current preset available")
return self.reference_manager.delete_reference(reference_name, current_preset) return self.reference_manager.delete_reference(reference_name, current_preset)
def delete_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
"""Delete a saved calibration for the given preset."""
self.calibration_manager.delete_calibration(preset, calibration_name)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Acquisition integration # Acquisition integration
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@ -369,4 +373,4 @@ class VNASettingsManager:
current_sweep=current_sweep_number, current_sweep=current_sweep_number,
timeout=timeout_seconds timeout=timeout_seconds
) )
return None return None

View File

@ -74,15 +74,6 @@ else
log_warning "Consider creating a virtual environment: python3 -m venv venv" log_warning "Consider creating a virtual environment: python3 -m venv venv"
fi fi
# Check configuration files
if [ ! -f "vna_system/api/api_config.json" ]; then
log_warning "API config not found. Using defaults."
fi
if [ ! -f "vna_system/core/processing/config.json" ]; then
log_warning "Processing config not found. Some processors may not work."
fi
# Start the server # Start the server
log_info "Starting VNA System API server..." log_info "Starting VNA System API server..."
log_info "Press Ctrl+C to stop the server" log_info "Press Ctrl+C to stop the server"

View File

@ -518,61 +518,40 @@
margin-top: var(--space-3); margin-top: var(--space-3);
} }
/* Current Reference Info */ /* Reference details */
.current-reference-info {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--color-border);
}
.reference-info-card {
background: var(--color-gray-800);
border: 1px solid var(--color-gray-600);
border-radius: var(--radius-lg);
padding: var(--space-4);
}
.reference-info-card h5 {
margin: 0 0 var(--space-3) 0;
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
color: var(--color-gray-300);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.reference-details { .reference-details {
margin-top: var(--space-3);
padding: var(--space-3);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
background: var(--bg-surface);
box-shadow: var(--shadow-sm);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-2); gap: var(--space-2);
background: var(--color-gray-900);
padding: var(--space-3);
border-radius: var(--radius-md);
border: 1px solid var(--color-gray-700);
} }
.reference-name { .reference-details__row {
font-size: var(--text-base); display: flex;
font-weight: var(--weight-semibold); gap: var(--space-2);
color: var(--color-white); font-size: var(--font-size-sm);
align-items: baseline;
} }
.reference-timestamp { .reference-details__label {
font-size: var(--text-sm); min-width: 90px;
color: var(--color-gray-300); color: var(--text-secondary);
font-family: var(--font-mono); font-weight: var(--font-weight-medium);
} }
.reference-description { .reference-details__value {
margin: 0; color: var(--text-primary);
font-size: var(--text-sm); flex: 1 1 auto;
color: var(--color-gray-400); word-break: break-word;
font-style: italic;
} }
.reference-description:empty::before { .reference-details__row--description {
content: "Описание не указано"; align-items: flex-start;
opacity: 0.7;
} }
/* Responsive adjustments */ /* Responsive adjustments */

View File

@ -121,17 +121,21 @@ export class AcquisitionManager {
async updateStatus() { async updateStatus() {
try { try {
const status = await apiGet(API.ACQUISITION.STATUS); const status = await apiGet(API.ACQUISITION.STATUS);
const normalized = { ...status, connected: true };
this.currentStatus = status; this.currentStatus = normalized;
this.updateUI(status); this.updateUI(normalized);
} catch (error) { } catch (error) {
console.error('Error getting acquisition status:', error); console.error('Error getting acquisition status:', error);
this.updateUI({ const fallback = {
running: false, running: false,
paused: false, paused: false,
continuous_mode: true, continuous_mode: true,
sweep_count: 0 sweep_count: 0,
}); connected: false
};
this.currentStatus = fallback;
this.updateUI(fallback);
} }
} }
@ -176,6 +180,20 @@ export class AcquisitionManager {
this.elements.singleSweepBtn.disabled = false; this.elements.singleSweepBtn.disabled = false;
} }
const systemStatusEl = document.getElementById('systemStatus');
if (systemStatusEl) {
if (status.connected === false) {
systemStatusEl.textContent = 'Не подключено';
} else {
const mode = status.continuous_mode ? 'Непрерывный' : 'Одиночный';
let suffix = ' (остановлен)';
if (status.running) {
suffix = status.paused ? ' (пауза)' : ' (запущен)';
}
systemStatusEl.textContent = `Режим: ${mode}${suffix}`;
}
}
// Update sweep count in header if available // Update sweep count in header if available
const sweepCountEl = document.getElementById('sweepCount'); const sweepCountEl = document.getElementById('sweepCount');
if (sweepCountEl) { if (sweepCountEl) {

View File

@ -75,6 +75,7 @@ export const API = {
CALIBRATION_START: `${API_BASE}/settings/calibration/start`, CALIBRATION_START: `${API_BASE}/settings/calibration/start`,
CALIBRATION_SAVE: `${API_BASE}/settings/calibration/save`, CALIBRATION_SAVE: `${API_BASE}/settings/calibration/save`,
CALIBRATION_SET: `${API_BASE}/settings/calibration/set`, CALIBRATION_SET: `${API_BASE}/settings/calibration/set`,
CALIBRATION_DELETE: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}`,
CALIBRATION_ADD_STANDARD: `${API_BASE}/settings/calibration/add-standard`, CALIBRATION_ADD_STANDARD: `${API_BASE}/settings/calibration/add-standard`,
CALIBRATION_STANDARDS_PLOTS: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}/standards-plots`, CALIBRATION_STANDARDS_PLOTS: (name) => `${API_BASE}/settings/calibration/${encodeURIComponent(name)}/standards-plots`,

View File

@ -76,6 +76,7 @@ export class SettingsManager {
calibrationDropdown: document.getElementById('calibrationDropdown'), calibrationDropdown: document.getElementById('calibrationDropdown'),
setCalibrationBtn: document.getElementById('setCalibrationBtn'), setCalibrationBtn: document.getElementById('setCalibrationBtn'),
viewPlotsBtn: document.getElementById('viewPlotsBtn'), viewPlotsBtn: document.getElementById('viewPlotsBtn'),
deleteCalibrationBtn: document.getElementById('deleteCalibrationBtn'),
viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'), viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
// Modal // Modal
@ -91,6 +92,7 @@ export class SettingsManager {
setReferenceBtn: document.getElementById('setReferenceBtn'), setReferenceBtn: document.getElementById('setReferenceBtn'),
clearReferenceBtn: document.getElementById('clearReferenceBtn'), clearReferenceBtn: document.getElementById('clearReferenceBtn'),
deleteReferenceBtn: document.getElementById('deleteReferenceBtn'), deleteReferenceBtn: document.getElementById('deleteReferenceBtn'),
previewReferenceBtn: document.getElementById('previewReferenceBtn'),
currentReferenceInfo: document.getElementById('currentReferenceInfo'), currentReferenceInfo: document.getElementById('currentReferenceInfo'),
currentReferenceName: document.getElementById('currentReferenceName'), currentReferenceName: document.getElementById('currentReferenceName'),
currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'), currentReferenceTimestamp: document.getElementById('currentReferenceTimestamp'),
@ -99,6 +101,7 @@ export class SettingsManager {
// Status // Status
presetCount: document.getElementById('presetCount'), presetCount: document.getElementById('presetCount'),
calibrationCount: document.getElementById('calibrationCount'), calibrationCount: document.getElementById('calibrationCount'),
referenceCount: document.getElementById('referenceCount'),
systemStatus: document.getElementById('systemStatus') systemStatus: document.getElementById('systemStatus')
}; };
@ -158,7 +161,7 @@ export class SettingsManager {
} catch (e) { } catch (e) {
console.error('Status load failed:', e); console.error('Status load failed:', e);
if (this.elements.systemStatus) { if (this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Ошибка'; this.elements.systemStatus.textContent = 'Не подключено';
} }
this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы'); this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы');
return null; return null;
@ -191,7 +194,7 @@ export class SettingsManager {
await this.calibrationManager.loadWorkingCalibration(); await this.calibrationManager.loadWorkingCalibration();
if (!status && this.elements.systemStatus) { if (!status && this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Готово'; this.elements.systemStatus.textContent = 'Не подключено';
} }
const effectiveStatus = { const effectiveStatus = {
@ -206,6 +209,9 @@ export class SettingsManager {
if (this.elements.calibrationCount) { if (this.elements.calibrationCount) {
this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-'; this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-';
} }
if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = '-';
}
} }
this.updateHeaderSummary(effectiveStatus); this.updateHeaderSummary(effectiveStatus);
@ -227,10 +233,10 @@ export class SettingsManager {
if (this.elements.calibrationCount) { if (this.elements.calibrationCount) {
this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-'; this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-';
} }
if (this.elements.systemStatus) { if (this.elements.referenceCount) {
this.elements.systemStatus.textContent = 'Готово'; const count = this.referenceManager?.availableReferences?.length;
this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-';
} }
this.updateHeaderSummary(status); this.updateHeaderSummary(status);
this.updateReferenceSummary(this.referenceManager.getCurrentReference()); this.updateReferenceSummary(this.referenceManager.getCurrentReference());
} }

View File

@ -5,7 +5,7 @@
import { Debouncer, RequestGuard, ButtonState } from '../utils.js'; import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
import { renderIcons } from '../icons.js'; import { renderIcons } from '../icons.js';
import { apiGet, apiPost, buildUrl } from '../api-client.js'; import { apiGet, apiPost, apiDelete, buildUrl } from '../api-client.js';
import { API, TIMING, CALIBRATION_STANDARDS, NOTIFICATION_TYPES } from '../constants.js'; import { API, TIMING, CALIBRATION_STANDARDS, NOTIFICATION_TYPES } from '../constants.js';
const { SUCCESS, ERROR, INFO } = NOTIFICATION_TYPES; const { SUCCESS, ERROR, INFO } = NOTIFICATION_TYPES;
@ -25,6 +25,7 @@ export class CalibrationManager {
this.handleSaveCalibration = this.handleSaveCalibration.bind(this); this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
this.handleSetCalibration = this.handleSetCalibration.bind(this); this.handleSetCalibration = this.handleSetCalibration.bind(this);
this.handleCalibrationChange = this.handleCalibrationChange.bind(this); this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
this.handleDeleteCalibration = this.handleDeleteCalibration.bind(this);
} }
init(elements) { init(elements) {
@ -33,6 +34,7 @@ export class CalibrationManager {
this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration); this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration);
this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange); this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration); this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
this.elements.deleteCalibrationBtn?.addEventListener('click', this.handleDeleteCalibration);
this.elements.calibrationNameInput?.addEventListener('input', () => { this.elements.calibrationNameInput?.addEventListener('input', () => {
const hasName = this.elements.calibrationNameInput.value.trim().length > 0; const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
@ -46,6 +48,7 @@ export class CalibrationManager {
this.elements.saveCalibrationBtn?.removeEventListener('click', this.handleSaveCalibration); this.elements.saveCalibrationBtn?.removeEventListener('click', this.handleSaveCalibration);
this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange); this.elements.calibrationDropdown?.removeEventListener('change', this.handleCalibrationChange);
this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration); this.elements.setCalibrationBtn?.removeEventListener('click', this.handleSetCalibration);
this.elements.deleteCalibrationBtn?.removeEventListener('click', this.handleDeleteCalibration);
this.resetCaptureState(); this.resetCaptureState();
} }
@ -91,6 +94,9 @@ export class CalibrationManager {
dd.disabled = true; dd.disabled = true;
this.elements.setCalibrationBtn.disabled = true; this.elements.setCalibrationBtn.disabled = true;
this.elements.viewPlotsBtn.disabled = true; this.elements.viewPlotsBtn.disabled = true;
if (this.elements.deleteCalibrationBtn) {
this.elements.deleteCalibrationBtn.disabled = true;
}
return; return;
} }
@ -105,6 +111,9 @@ export class CalibrationManager {
dd.disabled = false; dd.disabled = false;
this.elements.setCalibrationBtn.disabled = true; this.elements.setCalibrationBtn.disabled = true;
this.elements.viewPlotsBtn.disabled = true; this.elements.viewPlotsBtn.disabled = true;
if (this.elements.deleteCalibrationBtn) {
this.elements.deleteCalibrationBtn.disabled = true;
}
} }
updateWorking(working) { updateWorking(working) {
@ -195,6 +204,9 @@ export class CalibrationManager {
const v = this.elements.calibrationDropdown.value; const v = this.elements.calibrationDropdown.value;
this.elements.setCalibrationBtn.disabled = !v; this.elements.setCalibrationBtn.disabled = !v;
this.elements.viewPlotsBtn.disabled = !v; this.elements.viewPlotsBtn.disabled = !v;
if (this.elements.deleteCalibrationBtn) {
this.elements.deleteCalibrationBtn.disabled = !v;
}
} }
async handleStartCalibration() { async handleStartCalibration() {
@ -233,6 +245,7 @@ export class CalibrationManager {
const btn = document.querySelector(`[data-standard="${standard}"]`); const btn = document.querySelector(`[data-standard="${standard}"]`);
ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Снятие...' }); ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Снятие...' });
this.toggleStandardButtons(true);
this.notify(INFO, 'Снятие стандарта', `Снимается стандарт ${standard.toUpperCase()}...`); this.notify(INFO, 'Снятие стандарта', `Снимается стандарт ${standard.toUpperCase()}...`);
@ -246,11 +259,29 @@ export class CalibrationManager {
console.error('Capture standard failed:', e); console.error('Capture standard failed:', e);
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось снять стандарт калибровки'); this.notify(ERROR, 'Ошибка калибровки', 'Не удалось снять стандарт калибровки');
this.resetCaptureState(standard); this.resetCaptureState(standard);
} finally {
this.toggleStandardButtons(false);
} }
}), TIMING.DEBOUNCE_CALIBRATION }), TIMING.DEBOUNCE_CALIBRATION
); );
} }
toggleStandardButtons(disabled) {
const buttons = this.elements.calibrationStandards?.querySelectorAll('.calibration-standard-btn');
if (!buttons) return;
buttons.forEach(btn => {
if (disabled) {
btn.dataset.prevDisabled = btn.disabled ? '1' : '0';
btn.disabled = true;
} else {
if (btn.dataset.prevDisabled !== undefined) {
btn.disabled = btn.dataset.prevDisabled === '1';
delete btn.dataset.prevDisabled;
}
}
});
}
async handleSaveCalibration() { async handleSaveCalibration() {
const name = this.elements.calibrationNameInput.value.trim(); const name = this.elements.calibrationNameInput.value.trim();
if (!name) return; if (!name) return;
@ -312,6 +343,48 @@ export class CalibrationManager {
); );
} }
async handleDeleteCalibration() {
const name = this.elements.calibrationDropdown.value;
if (!name || !this.currentPreset) {
return;
}
if (!confirm(`Удалить калибровку «${name}»? Это действие необратимо.`)) {
return;
}
this.debouncer.debounce('delete-calibration', () =>
this.reqGuard.runExclusive('delete-calibration', async () => {
try {
if (this.elements.deleteCalibrationBtn) {
ButtonState.set(this.elements.deleteCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Удаление...' });
}
const url = buildUrl(API.SETTINGS.CALIBRATION_DELETE(name), {
preset_filename: this.currentPreset.filename
});
const result = await apiDelete(url);
this.notify(SUCCESS, 'Калибровка удалена', result.message);
await this.loadCalibrations();
await this.loadWorkingCalibration();
if (this.onCalibrationSet) {
await this.onCalibrationSet();
}
} catch (e) {
console.error('Delete calibration failed:', e);
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось удалить калибровку');
} finally {
if (this.elements.deleteCalibrationBtn) {
ButtonState.set(this.elements.deleteCalibrationBtn, { state: 'normal', icon: 'trash-2', text: 'Удалить' });
}
}
}), TIMING.DEBOUNCE_CALIBRATION
);
}
updateStatus(status) { updateStatus(status) {
if (status.current_calibration) { if (status.current_calibration) {
this.currentCalibration = status.current_calibration; this.currentCalibration = status.current_calibration;

View File

@ -68,14 +68,57 @@ export class PresetManager {
} }
formatDisplay(p) { formatDisplay(p) {
let s = `${p.filename} (${p.mode})`; if (!p) return '';
if (p.start_freq && p.stop_freq) {
const startMHz = (p.start_freq / 1e6).toFixed(0); const parts = [];
const stopMHz = (p.stop_freq / 1e6).toFixed(0); if (p.mode) {
s += ` - ${startMHz}-${stopMHz}MHz`; parts.push(p.mode.toUpperCase());
} }
if (p.points) s += `, ${p.points}pts`;
return s; const start = this.formatFrequency(p.start_freq);
const stop = this.formatFrequency(p.stop_freq);
if (start && stop) {
parts.push(`${start} ${stop}`);
} else if (start || stop) {
parts.push(start || stop);
}
if (p.points) {
parts.push(`${p.points} точек`);
}
const bandwidth = this.formatFrequency(p.bandwidth);
if (bandwidth) {
parts.push(`ПП ${bandwidth}`);
}
if (!parts.length && p.filename) {
return p.filename;
}
return parts.join(' • ');
}
formatFrequency(value) {
if (value === null || value === undefined) return null;
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) return null;
const units = [
{ divider: 1e9, suffix: 'ГГц' },
{ divider: 1e6, suffix: 'МГц' },
{ divider: 1e3, suffix: 'кГц' },
];
for (const unit of units) {
if (numeric >= 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)} Гц`;
} }
handlePresetChange() { handlePresetChange() {
@ -114,7 +157,7 @@ export class PresetManager {
updateStatus(status) { updateStatus(status) {
if (status.current_preset) { if (status.current_preset) {
this.currentPreset = status.current_preset; this.currentPreset = status.current_preset;
this.elements.currentPreset.textContent = status.current_preset.filename; this.elements.currentPreset.textContent = this.formatDisplay(status.current_preset);
} else { } else {
this.currentPreset = null; this.currentPreset = null;
this.elements.currentPreset.textContent = 'Нет'; this.elements.currentPreset.textContent = 'Нет';
@ -129,7 +172,7 @@ export class PresetManager {
setCurrentPresetDirect(preset) { setCurrentPresetDirect(preset) {
this.currentPreset = preset; this.currentPreset = preset;
if (this.elements.currentPreset) { if (this.elements.currentPreset) {
this.elements.currentPreset.textContent = preset ? preset.filename : 'Нет'; this.elements.currentPreset.textContent = preset ? this.formatDisplay(preset) : 'Нет';
} }
this.syncSelectedPreset(); this.syncSelectedPreset();
} }

View File

@ -7,7 +7,7 @@ import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
import { apiGet, apiPost, apiDelete, buildUrl } from '../api-client.js'; import { apiGet, apiPost, apiDelete, buildUrl } from '../api-client.js';
import { API, TIMING, NOTIFICATION_TYPES } from '../constants.js'; import { API, TIMING, NOTIFICATION_TYPES } from '../constants.js';
const { SUCCESS, ERROR, WARNING } = NOTIFICATION_TYPES; const { SUCCESS, ERROR, WARNING, INFO } = NOTIFICATION_TYPES;
export class ReferenceManager { export class ReferenceManager {
constructor(notifications) { constructor(notifications) {
@ -25,6 +25,7 @@ export class ReferenceManager {
this.handleSetReference = this.handleSetReference.bind(this); this.handleSetReference = this.handleSetReference.bind(this);
this.handleClearReference = this.handleClearReference.bind(this); this.handleClearReference = this.handleClearReference.bind(this);
this.handleDeleteReference = this.handleDeleteReference.bind(this); this.handleDeleteReference = this.handleDeleteReference.bind(this);
this.handlePreviewReference = this.handlePreviewReference.bind(this);
} }
init(elements) { init(elements) {
@ -34,6 +35,7 @@ export class ReferenceManager {
this.elements.setReferenceBtn?.addEventListener('click', this.handleSetReference); this.elements.setReferenceBtn?.addEventListener('click', this.handleSetReference);
this.elements.clearReferenceBtn?.addEventListener('click', this.handleClearReference); this.elements.clearReferenceBtn?.addEventListener('click', this.handleClearReference);
this.elements.deleteReferenceBtn?.addEventListener('click', this.handleDeleteReference); this.elements.deleteReferenceBtn?.addEventListener('click', this.handleDeleteReference);
this.elements.previewReferenceBtn?.addEventListener('click', this.handlePreviewReference);
} }
destroy() { destroy() {
@ -42,6 +44,13 @@ export class ReferenceManager {
this.elements.setReferenceBtn?.removeEventListener('click', this.handleSetReference); this.elements.setReferenceBtn?.removeEventListener('click', this.handleSetReference);
this.elements.clearReferenceBtn?.removeEventListener('click', this.handleClearReference); this.elements.clearReferenceBtn?.removeEventListener('click', this.handleClearReference);
this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference); this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference);
this.elements.previewReferenceBtn?.removeEventListener('click', this.handlePreviewReference);
}
updateReferenceCount(count) {
if (this.elements.referenceCount) {
this.elements.referenceCount.textContent = typeof count === 'number' ? count : '-';
}
} }
async setCurrentPreset(preset) { async setCurrentPreset(preset) {
@ -65,6 +74,7 @@ export class ReferenceManager {
preset_filename: this.currentPreset.filename preset_filename: this.currentPreset.filename
}); });
this.availableReferences = await apiGet(referencesUrl); this.availableReferences = await apiGet(referencesUrl);
this.updateReferenceCount(this.availableReferences.length);
const currentUrl = buildUrl(API.SETTINGS.REFERENCE_CURRENT, { const currentUrl = buildUrl(API.SETTINGS.REFERENCE_CURRENT, {
preset_filename: this.currentPreset.filename preset_filename: this.currentPreset.filename
@ -77,6 +87,7 @@ export class ReferenceManager {
console.error('Failed to load references:', error); console.error('Failed to load references:', error);
this.renderDropdown([]); this.renderDropdown([]);
this.updateInfo(null); this.updateInfo(null);
this.updateReferenceCount(null);
} }
} }
@ -92,6 +103,7 @@ export class ReferenceManager {
const setBtn = this.elements.setReferenceBtn; const setBtn = this.elements.setReferenceBtn;
const deleteBtn = this.elements.deleteReferenceBtn; const deleteBtn = this.elements.deleteReferenceBtn;
const clearBtn = this.elements.clearReferenceBtn; const clearBtn = this.elements.clearReferenceBtn;
const previewBtn = this.elements.previewReferenceBtn;
if (references.length === 0) { if (references.length === 0) {
this.elements.referenceDropdown.innerHTML = '<option value="">Эталоны отсутствуют</option>'; this.elements.referenceDropdown.innerHTML = '<option value="">Эталоны отсутствуют</option>';
@ -99,6 +111,7 @@ export class ReferenceManager {
if (setBtn) setBtn.disabled = true; if (setBtn) setBtn.disabled = true;
if (clearBtn) clearBtn.disabled = true; if (clearBtn) clearBtn.disabled = true;
if (deleteBtn) deleteBtn.disabled = true; if (deleteBtn) deleteBtn.disabled = true;
if (previewBtn) previewBtn.disabled = true;
} else { } else {
this.elements.referenceDropdown.innerHTML = '<option value="">Выберите эталон...</option>'; this.elements.referenceDropdown.innerHTML = '<option value="">Выберите эталон...</option>';
@ -117,6 +130,7 @@ export class ReferenceManager {
if (setBtn) setBtn.disabled = false; if (setBtn) setBtn.disabled = false;
if (deleteBtn) deleteBtn.disabled = false; if (deleteBtn) deleteBtn.disabled = false;
if (previewBtn) previewBtn.disabled = false;
} }
this.updateButtons(); this.updateButtons();
@ -132,10 +146,12 @@ export class ReferenceManager {
const setBtn = this.elements.setReferenceBtn; const setBtn = this.elements.setReferenceBtn;
const deleteBtn = this.elements.deleteReferenceBtn; const deleteBtn = this.elements.deleteReferenceBtn;
const clearBtn = this.elements.clearReferenceBtn; const clearBtn = this.elements.clearReferenceBtn;
const previewBtn = this.elements.previewReferenceBtn;
if (setBtn) setBtn.disabled = !hasSelection; if (setBtn) setBtn.disabled = !hasSelection;
if (deleteBtn) deleteBtn.disabled = !hasSelection; if (deleteBtn) deleteBtn.disabled = !hasSelection;
if (clearBtn) clearBtn.disabled = !hasCurrent; if (clearBtn) clearBtn.disabled = !hasCurrent;
if (previewBtn) previewBtn.disabled = !(hasSelection || hasCurrent);
} }
updateInfo(reference) { updateInfo(reference) {
@ -148,11 +164,15 @@ export class ReferenceManager {
if (!reference) { if (!reference) {
this.elements.currentReferenceInfo.style.display = 'none'; this.elements.currentReferenceInfo.style.display = 'none';
if (this.elements.currentReferenceName) this.elements.currentReferenceName.textContent = '—';
if (this.elements.currentReferenceTimestamp) this.elements.currentReferenceTimestamp.textContent = '—';
if (this.elements.currentReferenceDescription) this.elements.currentReferenceDescription.textContent = '—';
this.onReferenceUpdated?.(null); this.onReferenceUpdated?.(null);
this.updateButtons();
return; return;
} }
this.elements.currentReferenceInfo.style.display = 'block'; this.elements.currentReferenceInfo.style.display = 'flex';
if (this.elements.currentReferenceName) { if (this.elements.currentReferenceName) {
this.elements.currentReferenceName.textContent = reference.name; this.elements.currentReferenceName.textContent = reference.name;
@ -166,10 +186,11 @@ export class ReferenceManager {
} }
if (this.elements.currentReferenceDescription) { if (this.elements.currentReferenceDescription) {
this.elements.currentReferenceDescription.textContent = reference.description || ''; this.elements.currentReferenceDescription.textContent = reference.description || '';
} }
this.onReferenceUpdated?.(reference); this.onReferenceUpdated?.(reference);
this.updateButtons();
} }
async handleCreateReference() { async handleCreateReference() {
@ -208,6 +229,51 @@ export class ReferenceManager {
this.updateButtons(); this.updateButtons();
} }
async handlePreviewReference() {
const dropdown = this.elements.referenceDropdown;
const selectedName = dropdown?.value ?? '';
const targetName = selectedName || this.currentReference?.name;
if (!targetName) {
this.notify(WARNING, 'Нет данных', 'Не выбран ни один эталон для просмотра');
return;
}
this.debouncer.debounce('preview-reference', () =>
this.reqGuard.runExclusive('preview-reference', async () => {
try {
const url = buildUrl(API.SETTINGS.REFERENCE_ITEM(targetName), {
preset_filename: this.currentPreset?.filename
});
const details = await apiGet(url);
const timestamp = details?.timestamp
? new Date(details.timestamp).toLocaleString()
: '—';
const description = details?.description ? details.description : '—';
const lines = [
`Имя: ${details?.name ?? targetName}`,
`Дата: ${timestamp}`,
`Описание: ${description}`
];
if (details?.metadata && Object.keys(details.metadata).length) {
const metaPairs = Object.entries(details.metadata)
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('; ');
lines.push(`Метаданные: ${metaPairs}`);
}
const message = lines.join(' • ');
this.notify(INFO, 'Просмотр эталона', message);
} catch (error) {
console.error('Preview reference failed:', error);
this.notify(ERROR, 'Ошибка эталона', 'Не удалось загрузить данные эталона');
}
})
);
}
async handleSetReference() { async handleSetReference() {
const referenceName = this.elements.referenceDropdown.value; const referenceName = this.elements.referenceDropdown.value;
if (!referenceName) return; if (!referenceName) return;
@ -263,7 +329,7 @@ export class ReferenceManager {
const referenceName = this.elements.referenceDropdown.value; const referenceName = this.elements.referenceDropdown.value;
if (!referenceName) return; if (!referenceName) return;
if (!confirm(`Are you sure you want to delete reference "${referenceName}"? This action cannot be undone.`)) { if (!confirm(`Удалить эталон «${referenceName}»? Это действие необратимо.`)) {
return; return;
} }
@ -292,6 +358,7 @@ export class ReferenceManager {
this.currentReference = null; this.currentReference = null;
this.renderDropdown([]); this.renderDropdown([]);
this.updateInfo(null); this.updateInfo(null);
this.updateReferenceCount(0);
} }
notify(type, title, message) { notify(type, title, message) {

View File

@ -246,6 +246,10 @@
<span data-icon="bar-chart-3"></span> <span data-icon="bar-chart-3"></span>
Показать графики Показать графики
</button> </button>
<button class="btn btn--secondary" id="deleteCalibrationBtn" disabled>
<span data-icon="trash-2"></span>
Удалить
</button>
</div> </div>
</div> </div>
</div> </div>
@ -278,19 +282,33 @@
<option value="">Эталоны отсутствуют</option> <option value="">Эталоны отсутствуют</option>
</select> </select>
<div class="reference-actions settings-action-group"> <div class="reference-actions settings-action-group">
<button class="btn btn--primary" id="setReferenceBtn" disabled> <button class="btn btn--primary" id="setReferenceBtn" disabled>
<span data-icon="check"></span> <span data-icon="check"></span>
Использовать Использовать
</button> </button>
<button class="btn btn--secondary" id="previewReferenceBtn" disabled> <button class="btn btn--secondary" id="previewReferenceBtn" disabled>
<span data-icon="bar-chart-3"></span> <span data-icon="bar-chart-3"></span>
Просмотр Просмотр
</button> </button>
<button class="btn btn--secondary" id="deleteReferenceBtn" disabled> <button class="btn btn--secondary" id="deleteReferenceBtn" disabled>
<span data-icon="trash-2"></span> <span data-icon="trash-2"></span>
Удалить Удалить
</button> </button>
</div> </div>
<div class="reference-details" id="currentReferenceInfo" style="display: none;">
<div class="reference-details__row">
<span class="reference-details__label">Имя:</span>
<span class="reference-details__value" id="currentReferenceName"></span>
</div>
<div class="reference-details__row">
<span class="reference-details__label">Дата:</span>
<span class="reference-details__value" id="currentReferenceTimestamp"></span>
</div>
<div class="reference-details__row reference-details__row--description">
<span class="reference-details__label">Описание:</span>
<span class="reference-details__value" id="currentReferenceDescription"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -304,21 +322,17 @@
<span class="status-label">Получено свипов:</span> <span class="status-label">Получено свипов:</span>
<span class="status-value" id="sweepCount">-</span> <span class="status-value" id="sweepCount">-</span>
</div> </div>
<div class="status-item">
<span class="status-label">Обработано точек:</span>
<span class="status-value" id="pointCount">-</span>
</div>
<div class="status-item">
<span class="status-label">Активные процессоры:</span>
<span class="status-value" id="processorCount">-</span>
</div>
<div class="status-item"> <div class="status-item">
<span class="status-label">Калибровок сохранено:</span> <span class="status-label">Калибровок сохранено:</span>
<span class="status-value" id="calibrationCount">-</span> <span class="status-value" id="calibrationCount">-</span>
</div> </div>
<div class="status-item">
<span class="status-label">Эталонов сохранено:</span>
<span class="status-value" id="referenceCount">-</span>
</div>
<div class="status-item"> <div class="status-item">
<span class="status-label">Статус системы:</span> <span class="status-label">Статус системы:</span>
<span class="status-value" id="systemStatus">Проверка...</span> <span class="status-value" id="systemStatus">Режим: —</span>
</div> </div>
</div> </div>
</div> </div>