translated to russian and removed internet deps

This commit is contained in:
Ayzen
2025-09-30 17:37:05 +03:00
parent b0c2d48c2b
commit 7c26285e7f
28 changed files with 4850 additions and 374 deletions

View File

@ -1 +0,0 @@
s21_start100_stop8800_points1000_bw1khz/bambambum

View File

@ -200,13 +200,24 @@ class VNADataAcquisition:
self._reset_sweep_state()
# Read until exactly one sweep is completed
if self._stop_event.is_set():
logger.debug("Stop requested before sweep start; aborting sweep replay")
return
# Read until exactly one sweep is completed. Honor stop requests only
# between sweeps so we do not break out mid-collection.
sweep_completed = False
while not sweep_completed and self._running and not self._stop_event.is_set():
while not sweep_completed:
if self._stop_event.is_set() and not self._collecting:
logger.debug("Stop requested with no active sweep; exiting loop")
break
dir_b = f.read(1)
if not dir_b:
# EOF reached; wait for more data to arrive on disk
logger.debug("EOF reached; waiting for more data")
if self._collecting:
logger.warning("EOF reached while sweep in progress; aborting partial sweep")
else:
logger.debug("EOF reached; waiting for more data")
time.sleep(0.1)
break

View File

@ -1,10 +1,10 @@
{
"open_air": false,
"open_air": true,
"axis": "abs",
"data_limitation": "ph_only_2",
"cut": 0.417,
"max": 0.6,
"gain": 0.0,
"data_limitation": "ph_only_1",
"cut": 1.291,
"max": 1.0,
"gain": 0.9,
"start_freq": 100.0,
"stop_freq": 8800.0,
"clear_history": false,

View File

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

View File

@ -1 +1 @@
s21_start100_stop8800_points1000_bw1khz/testet
s21_start100_stop8800_points1000_bw1khz/ффывфы

View File

@ -0,0 +1,7 @@
{
"name": "ффывфы",
"timestamp": "2025-09-30T16:45:34.508464",
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
"description": "",
"metadata": {}
}

View File

@ -1,8 +1,54 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#1e293b"/>
<path d="M8 24L12 12L16 20L20 8L24 16" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<circle cx="12" cy="12" r="1.5" fill="#22c55e"/>
<circle cx="16" cy="20" r="1.5" fill="#22c55e"/>
<circle cx="20" cy="8" r="1.5" fill="#22c55e"/>
<circle cx="24" cy="16" r="1.5" fill="#22c55e"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="547.20001"
height="655.85333"
viewBox="0 0 547.20001 655.85333"
sodipodi:docname="hv_full.eps"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="1"
id="page1"
width="547.20001"
height="655.85333"
margin="0"
bleed="0" />
</sodipodi:namedview>
<g
id="g1"
inkscape:groupmode="layer"
inkscape:label="1">
<g
id="group-R5">
<path
id="path2"
d="m 1588.49,4316.95 249.28,598.16 -609.76,-299.07 -609.748,299.07 249.269,-598.16 h 720.959"
style="fill:#316bbd;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,655.85333)" />
<path
id="path3"
d="m 1054.51,3561.77 57.85,288.06 c 76.05,0.71 123.36,-46.16 109.33,-116.37 -14.05,-70.3 -84.96,-140.86 -167.18,-171.69 z m -79.623,-15.87 c -76.129,-0.76 -123.489,46.12 -109.449,116.37 14.054,70.34 85.031,140.93 167.312,171.74 z M 4054.74,3347.77 c -134.5,668.72 -525.77,1219.84 -1050.1,1571.15 L 2777.31,3786.95 c 261.73,-233.42 452.84,-550.45 527.5,-920.69 C 3471.86,2037.79 2998.08,1229.04 2224.83,953.09 l 252.44,1257.01 c 64.09,319.2 -60.17,478.46 -354.97,419.26 l -909.34,-182.62 c 0,0 105.3,524.33 245.78,1223.85 64.11,319.24 -60.14,478.52 -354.96,419.31 C 663.801,4001.55 291.445,3581.76 141.855,3256.46 L 675.5,3363.62 0,0 l 751.922,151 355.118,1768.32 516.91,102.9 c 46.58,9.27 87.61,-31.82 78.26,-78.39 L 1366.97,274.52 1987.58,399.16 c 44.79,8.969 88.68,18.371 131.67,28.192 v 0 C 3458.18,698.031 4324.73,2005.53 4054.74,3347.77"
style="fill:#316bbd;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,655.85333)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,10 +1,66 @@
/* Acquisition Controls */
.acquisition-controls {
display: flex;
gap: var(--space-2);
gap: var(--space-4);
align-items: stretch;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: var(--space-3);
}
.acquisition-controls__buttons {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.acquisition-controls__buttons .btn {
min-width: 120px;
}
.acquisition-summary {
flex: 1 1 360px;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
background-color: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-left: auto;
}
.acquisition-summary .header-summary__item {
flex: 1 1 auto;
}
.acquisition-summary .header-summary__divider {
height: 32px;
}
@media (max-width: 1024px) {
.acquisition-summary {
flex: 1 1 100%;
}
}
@media (max-width: 640px) {
.acquisition-summary {
flex-wrap: wrap;
row-gap: var(--space-2);
}
.acquisition-summary .header-summary__divider {
display: none;
}
.acquisition-summary .header-summary__item {
flex: 1 1 45%;
}
}
.acquisition-status {
display: flex;
align-items: center;

View File

@ -1,3 +1,11 @@
/* Icons */
.icon {
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
display: inline-block;
}
/* Buttons */
.btn {
display: inline-flex;
@ -71,17 +79,17 @@
color: var(--text-primary);
}
.btn i {
.btn .icon {
width: 14px;
height: 14px;
}
.btn--sm i {
.btn--sm .icon {
width: 12px;
height: 12px;
}
.btn--lg i {
.btn--lg .icon {
width: 16px;
height: 16px;
}
@ -267,7 +275,7 @@
opacity: 0.8;
}
.chart-card__action i {
.chart-card__action .icon {
width: 14px;
height: 14px;
}
@ -517,7 +525,7 @@
background-color: var(--bg-surface-hover);
}
.notification__close i {
.notification__close .icon {
width: 16px;
height: 16px;
}

View File

@ -174,7 +174,7 @@ body {
background-color: var(--color-primary-900);
}
.nav-btn i {
.nav-btn .icon {
width: 16px;
height: 16px;
}

View File

@ -294,7 +294,7 @@
white-space: nowrap;
}
.calibration-standard-btn i {
.calibration-standard-btn .icon {
margin-right: var(--space-2);
}
@ -571,7 +571,7 @@
}
.reference-description:empty::before {
content: "No description provided";
content: "Описание не указано";
opacity: 0.7;
}

View File

@ -10,6 +10,7 @@ import { NotificationManager } from './modules/notifications.js';
import { StorageManager } from './modules/storage.js';
import { SettingsManager } from './modules/settings.js';
import { AcquisitionManager } from './modules/acquisition.js';
import { renderIcons } from './modules/icons.js';
/**
* Main Application Class
@ -65,10 +66,8 @@ class VNADashboard {
try {
console.log('Initializing VNA Dashboard...');
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Render SVG icons
renderIcons();
// Initialize modules in correct order
await this.initializeModules();
@ -85,16 +84,16 @@ class VNADashboard {
// Show welcome notification
this.notifications.show({
type: 'info',
title: 'Dashboard Ready',
message: 'Connected to VNA System. Waiting for sweep data...'
title: 'Панель готова',
message: 'Соединение с системой ВНА установлено. Ожидание данных свипа...'
});
} catch (error) {
console.error('Failed to initialize VNA Dashboard:', error);
this.notifications.show({
type: 'error',
title: 'Initialization Failed',
message: error.message || 'Failed to initialize dashboard'
title: 'Сбой инициализации',
message: error.message || 'Не удалось инициализировать панель'
});
}
}

View File

@ -56,60 +56,63 @@ export class AcquisitionManager {
}
async handleStartClick() {
let originalState;
try {
const originalState = setButtonLoading(this.elements.startBtn, true);
originalState = setButtonLoading(this.elements.startBtn, true);
const result = await apiPost(API.ACQUISITION.START);
if (result.success) {
this.notifications.show({ type: SUCCESS, title: 'Acquisition Started', message: result.message });
this.notifications.show({ type: SUCCESS, title: 'Сбор запущен', message: result.message });
await this.updateStatus();
} else {
this.notifications.show({ type: ERROR, title: 'Start Failed', message: result.error || 'Failed to start acquisition' });
this.notifications.show({ type: ERROR, title: 'Сбой запуска', message: result.error || 'Не удалось запустить сбор' });
}
} catch (error) {
console.error('Error starting acquisition:', error);
this.notifications.show({ type: ERROR, title: 'Start Failed', message: 'Failed to start acquisition' });
this.notifications.show({ type: ERROR, title: 'Сбой запуска', message: 'Не удалось запустить сбор' });
} finally {
setButtonLoading(this.elements.startBtn, false, originalState);
}
}
async handleStopClick() {
let originalState;
try {
const originalState = setButtonLoading(this.elements.stopBtn, true);
originalState = setButtonLoading(this.elements.stopBtn, true);
const result = await apiPost(API.ACQUISITION.STOP);
if (result.success) {
this.notifications.show({ type: SUCCESS, title: 'Acquisition Stopped', message: result.message });
this.notifications.show({ type: SUCCESS, title: 'Сбор остановлен', message: result.message });
await this.updateStatus();
} else {
this.notifications.show({ type: ERROR, title: 'Stop Failed', message: result.error || 'Failed to stop acquisition' });
this.notifications.show({ type: ERROR, title: 'Сбой остановки', message: result.error || 'Не удалось остановить сбор' });
}
} catch (error) {
console.error('Error stopping acquisition:', error);
this.notifications.show({ type: ERROR, title: 'Stop Failed', message: 'Failed to stop acquisition' });
this.notifications.show({ type: ERROR, title: 'Сбой остановки', message: 'Не удалось остановить сбор' });
} finally {
setButtonLoading(this.elements.stopBtn, false, originalState);
}
}
async handleSingleSweepClick() {
let originalState;
try {
const originalState = setButtonLoading(this.elements.singleSweepBtn, true);
originalState = setButtonLoading(this.elements.singleSweepBtn, true);
const result = await apiPost(API.ACQUISITION.SINGLE);
if (result.success) {
this.notifications.show({ type: SUCCESS, title: 'Single Sweep', message: result.message });
this.notifications.show({ type: SUCCESS, title: 'Одиночный свип', message: result.message });
await this.updateStatus();
} else {
this.notifications.show({ type: ERROR, title: 'Single Sweep Failed', message: result.error || 'Failed to trigger single sweep' });
this.notifications.show({ type: ERROR, title: 'Сбой одиночного свипа', message: result.error || 'Не удалось запустить одиночный свип' });
}
} catch (error) {
console.error('Error triggering single sweep:', error);
this.notifications.show({ type: ERROR, title: 'Single Sweep Failed', message: 'Failed to trigger single sweep' });
this.notifications.show({ type: ERROR, title: 'Сбой одиночного свипа', message: 'Не удалось запустить одиночный свип' });
} finally {
setButtonLoading(this.elements.singleSweepBtn, false, originalState);
}
@ -134,15 +137,15 @@ export class AcquisitionManager {
updateUI(status) {
// Update status text and indicator
let statusText = 'Idle';
let statusText = 'Ожидание';
let statusClass = 'status-indicator__dot--idle';
if (status.running) {
if (status.paused) {
statusText = 'Stopped';
statusText = 'Остановлено';
statusClass = 'status-indicator__dot--paused';
} else {
statusText = 'Running';
statusText = 'Выполняется';
statusClass = 'status-indicator__dot--running';
}
}
@ -157,7 +160,7 @@ export class AcquisitionManager {
// Update mode text
if (this.elements.modeText) {
this.elements.modeText.textContent = status.continuous_mode ? 'Continuous' : 'Single';
this.elements.modeText.textContent = status.continuous_mode ? 'Непрерывный' : 'Одиночный';
}
// Update button states
@ -170,7 +173,7 @@ export class AcquisitionManager {
}
if (this.elements.singleSweepBtn) {
this.elements.singleSweepBtn.disabled = !status.running;
this.elements.singleSweepBtn.disabled = false;
}
// Update sweep count in header if available

View File

@ -4,6 +4,7 @@
*/
import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
import { renderIcons } from './icons.js';
import { ChartSettingsManager } from './charts/chart-settings.js';
import {
defaultPlotlyLayout,
@ -76,7 +77,9 @@ export class ChartManager {
} catch (e) {
console.error('Error adding chart result:', e);
this.notifications?.show?.({
type: 'error', title: 'Chart Error', message: `Failed to update chart`
type: 'error',
title: 'Ошибка графика',
message: 'Не удалось обновить график'
});
}
}
@ -164,18 +167,18 @@ export class ChartManager {
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="bar-chart-3" class="chart-card__icon"></i>
<span data-icon="bar-chart-3" class="chart-card__icon"></span>
${formatProcessorName(processorId)}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
<i data-lucide="expand"></i>
<span data-icon="expand"></span>
</button>
<button class="chart-card__action" data-action="download" title="Download">
<i data-lucide="download"></i>
<span data-icon="download"></span>
</button>
<button class="chart-card__action" data-action="hide" title="Hide">
<i data-lucide="eye-off"></i>
<span data-icon="eye-off"></span>
</button>
</div>
</div>
@ -199,7 +202,7 @@ export class ChartManager {
this.setupChartCardEvents(card, processorId);
this.updateChartSettings(processorId);
if (typeof lucide !== 'undefined') lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
renderIcons(card);
return card;
}
@ -297,15 +300,15 @@ export class ChartManager {
this.notifications?.show?.({
type: 'success',
title: 'Download Complete',
message: `Downloaded ${formatProcessorName(id)} plot and data`
title: 'Скачивание завершено',
message: `Скачаны график и данные ${formatProcessorName(id)}`
});
} catch (e) {
console.error('Chart download failed:', e);
this.notifications?.show?.({
type: 'error',
title: 'Download Failed',
message: 'Failed to download chart data'
title: 'Ошибка скачивания',
message: 'Не удалось скачать данные графика'
});
}
}

View File

@ -4,6 +4,7 @@
*/
import { createParameterControl } from '../utils.js';
import { renderIcons } from '../icons.js';
export class ChartSettingsManager {
constructor() {
@ -42,7 +43,7 @@ export class ChartSettingsManager {
return;
}
settingsContainer.innerHTML = '<div class="settings-empty">No settings available</div>';
settingsContainer.innerHTML = '<div class="settings-empty">Настройки недоступны</div>';
return;
}
@ -62,9 +63,7 @@ export class ChartSettingsManager {
});
}
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
renderIcons(settingsContainer);
}
setupEvents(settingsContainer, processorId) {

View File

@ -68,6 +68,7 @@ export const API = {
SETTINGS: {
PRESETS: `${API_BASE}/settings/presets`,
PRESET_SET: `${API_BASE}/settings/preset/set`,
PRESET_CURRENT: `${API_BASE}/settings/preset/current`,
STATUS: `${API_BASE}/settings/status`,
CALIBRATIONS: `${API_BASE}/settings/calibrations`,

View File

@ -0,0 +1,257 @@
const SVG_NS = 'http://www.w3.org/2000/svg';
const ICONS = {
activity: {
viewBox: '0 0 24 24',
elements: [
{ type: 'polyline', attrs: { points: '22 12 18 12 15 21 9 3 6 12 2 12' } }
]
},
'bar-chart-3': {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M3 3v18h18' } },
{ type: 'line', attrs: { x1: 9, y1: 9, x2: 9, y2: 21 } },
{ type: 'line', attrs: { x1: 13, y1: 6, x2: 13, y2: 21 } },
{ type: 'line', attrs: { x1: 17, y1: 12, x2: 17, y2: 21 } }
]
},
settings: {
viewBox: '0 0 24 24',
elements: [
{
type: 'path',
attrs: {
d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'
}
},
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 3.25 } }
]
},
play: {
viewBox: '0 0 24 24',
elements: [
{ type: 'polygon', attrs: { points: '9 5 19 12 9 19 9 5' } }
]
},
square: {
viewBox: '0 0 24 24',
elements: [
{ type: 'rect', attrs: { x: 5, y: 5, width: 14, height: 14, rx: 2 } }
]
},
zap: {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M13 2 3 14h7l-1 8 10-12h-7l1-8z' } }
]
},
check: {
viewBox: '0 0 24 24',
elements: [
{ type: 'polyline', attrs: { points: '4 12 10 18 20 6' } }
]
},
'check-circle': {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 9 } },
{ type: 'polyline', attrs: { points: '8 12 11 15 16 9' } }
]
},
save: {
viewBox: '0 0 24 24',
elements: [
{ type: 'rect', attrs: { x: 5, y: 3, width: 14, height: 18, rx: 2 } },
{ type: 'line', attrs: { x1: 5, y1: 9, x2: 19, y2: 9 } },
{ type: 'rect', attrs: { x: 9, y: 13, width: 6, height: 4, rx: 1 } }
]
},
target: {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 1.5 } },
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 5 } },
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 9 } }
]
},
'download-cloud': {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M6 19h11a4 4 0 0 0 .4-8A6 6 0 0 0 6 7a5 5 0 0 0-.5 10z' } },
{ type: 'line', attrs: { x1: 12, y1: 11, x2: 12, y2: 17 } },
{ type: 'polyline', attrs: { points: '9 14 12 17 15 14' } }
]
},
x: {
viewBox: '0 0 24 24',
elements: [
{ type: 'line', attrs: { x1: 18, y1: 6, x2: 6, y2: 18 } },
{ type: 'line', attrs: { x1: 6, y1: 6, x2: 18, y2: 18 } }
]
},
'trash-2': {
viewBox: '0 0 24 24',
elements: [
{ type: 'polyline', attrs: { points: '3 6 5 6 21 6' } },
{ type: 'path', attrs: { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' } },
{ type: 'line', attrs: { x1: 10, y1: 11, x2: 10, y2: 17 } },
{ type: 'line', attrs: { x1: 14, y1: 11, x2: 14, y2: 17 } },
{ type: 'path', attrs: { d: 'M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2' } }
]
},
expand: {
viewBox: '0 0 24 24',
elements: [
{ type: 'polyline', attrs: { points: '15 3 21 3 21 9' } },
{ type: 'polyline', attrs: { points: '9 21 3 21 3 15' } },
{ type: 'line', attrs: { x1: 21, y1: 3, x2: 14, y2: 10 } },
{ type: 'line', attrs: { x1: 3, y1: 21, x2: 10, y2: 14 } }
]
},
download: {
viewBox: '0 0 24 24',
elements: [
{ type: 'line', attrs: { x1: 12, y1: 3, x2: 12, y2: 17 } },
{ type: 'polyline', attrs: { points: '6 13 12 19 18 13' } },
{ type: 'line', attrs: { x1: 5, y1: 21, x2: 19, y2: 21 } }
]
},
upload: {
viewBox: '0 0 24 24',
elements: [
{ type: 'line', attrs: { x1: 12, y1: 21, x2: 12, y2: 7 } },
{ type: 'polyline', attrs: { points: '6 11 12 5 18 11' } },
{ type: 'line', attrs: { x1: 5, y1: 5, x2: 19, y2: 5 } }
]
},
'alert-triangle': {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M10.5 3.5L2.2 18a2 2 0 0 0 1.8 3h16a2 2 0 0 0 1.8-3L13.5 3.5a2 2 0 0 0-3 0z' } },
{ type: 'line', attrs: { x1: 12, y1: 9, x2: 12, y2: 13.5 } },
{ type: 'circle', attrs: { cx: 12, cy: 17, r: 0.75 } }
]
},
'eye-off': {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M3 3l18 18' } },
{ type: 'path', attrs: { d: 'M9.5 9.5A4 4 0 0 0 12 16a4 4 0 0 0 3.5-6.5' } },
{ type: 'path', attrs: { d: 'M7.2 7.5C4.8 8.9 3 12 3 12s2.7 5 9 5c1.5 0 2.8-.3 3.9-.8' } },
{ type: 'path', attrs: { d: 'M17.2 10.2C18.7 9.2 21 12 21 12s-2.7 5-9 5' } }
]
},
'alert-circle': {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 10 } },
{ type: 'line', attrs: { x1: 12, y1: 7, x2: 12, y2: 13 } },
{ type: 'circle', attrs: { cx: 12, cy: 17, r: 1 } }
]
},
loader: {
viewBox: '0 0 24 24',
elements: [
{ type: 'path', attrs: { d: 'M21 12a9 9 0 1 1-9-9' } }
]
},
info: {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 9 } },
{ type: 'line', attrs: { x1: 12, y1: 8, x2: 12, y2: 12 } },
{ type: 'circle', attrs: { cx: 12, cy: 16, r: 0.75 } }
]
},
clock: {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 9 } },
{ type: 'line', attrs: { x1: 12, y1: 7, x2: 12, y2: 12 } },
{ type: 'line', attrs: { x1: 12, y1: 12, x2: 16, y2: 14 } }
]
},
radio: {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 2 } },
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 6 } },
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 10 } }
]
}
};
const DEFAULT_ICON = {
viewBox: '0 0 24 24',
elements: [
{ type: 'circle', attrs: { cx: 12, cy: 12, r: 9 } },
{ type: 'line', attrs: { x1: 12, y1: 8, x2: 12, y2: 12 } },
{ type: 'circle', attrs: { cx: 12, cy: 16, r: 0.5 } }
]
};
function createSvgElement(definition) {
const element = document.createElementNS(SVG_NS, definition.type);
Object.entries(definition.attrs || {}).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
}
function buildIcon(name, options = {}) {
const definition = ICONS[name] || DEFAULT_ICON;
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('viewBox', definition.viewBox || '0 0 24 24');
const size = options.size || 24;
svg.setAttribute('width', size);
svg.setAttribute('height', size);
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', options.strokeWidth || 1.5);
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
(definition.elements || []).forEach(elementDef => {
svg.appendChild(createSvgElement(elementDef));
});
return svg;
}
export function renderIcons(root = document) {
if (!root) {
return;
}
const candidates = root.querySelectorAll('[data-icon]:not([data-icon-rendered]), [data-lucide]:not([data-icon-rendered])');
candidates.forEach(node => {
const iconName = node.getAttribute('data-icon') || node.getAttribute('data-lucide') || '';
const sizeAttr = node.getAttribute('data-icon-size');
const strokeAttr = node.getAttribute('data-icon-stroke');
const svg = buildIcon(iconName.trim().toLowerCase(), {
size: sizeAttr ? Number(sizeAttr) : undefined,
strokeWidth: strokeAttr ? Number(strokeAttr) : undefined
});
// Transfer non-icon attributes
node.getAttributeNames()
.filter(attr => !['data-icon', 'data-lucide', 'data-icon-size', 'data-icon-stroke', 'data-icon-rendered', 'class'].includes(attr))
.forEach(attr => {
svg.setAttribute(attr, node.getAttribute(attr));
});
svg.classList.add('icon');
node.classList.forEach(className => svg.classList.add(className));
node.replaceWith(svg);
});
}
export function getAvailableIcons() {
return Object.keys(ICONS);
}

View File

@ -4,6 +4,7 @@
*/
import { escapeHtml } from './utils.js';
import { renderIcons } from './icons.js';
export class NotificationManager {
constructor() {
@ -166,10 +167,7 @@ export class NotificationManager {
}, notification.timeout);
}
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
renderIcons(notification.element);
console.log(` Showing ${notification.type} notification:`, notification.title);
}
@ -182,7 +180,7 @@ export class NotificationManager {
element.className = `notification ${notification.config.class}`;
element.dataset.id = notification.id;
const iconHtml = `<i data-lucide="${notification.config.icon}" class="notification__icon"></i>`;
const iconHtml = `<span data-icon="${notification.config.icon}" class="notification__icon"></span>`;
const titleHtml = notification.title ?
`<div class="notification__title">${escapeHtml(notification.title)}</div>` : '';
@ -194,8 +192,8 @@ export class NotificationManager {
this.createActionsHtml(notification.actions) : '';
const closeHtml = `
<button class="notification__close" data-action="close" title="Close">
<i data-lucide="x"></i>
<button class="notification__close" data-action="close" title="Закрыть">
<span data-icon="x"></span>
</button>
`;
@ -225,7 +223,7 @@ export class NotificationManager {
const buttonClass = action.primary ? 'btn btn--primary btn--sm' : 'btn btn--ghost btn--sm';
return `
<button class="${buttonClass}" data-action="custom" data-action-id="${action.id}">
${action.icon ? `<i data-lucide="${action.icon}"></i>` : ''}
${action.icon ? `<span data-icon="${action.icon}"></span>` : ''}
${escapeHtml(action.label)}
</button>
`;

View File

@ -7,6 +7,7 @@ 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 { renderIcons } from './icons.js';
import {
createPlotlyPlot,
togglePlotlyFullscreen,
@ -53,7 +54,7 @@ export class SettingsManager {
console.log('Settings Manager initialized');
} catch (err) {
console.error('Settings Manager init failed:', err);
this.notify(ERROR, 'Settings Error', 'Failed to initialize settings');
this.notify(ERROR, 'Ошибка настроек', 'Не удалось инициализировать модуль настроек');
}
}
@ -115,18 +116,21 @@ export class SettingsManager {
// Setup callbacks
this.presetManager.onPresetChanged = async () => {
await this.loadStatus();
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 () => {
await this.loadStatus();
const status = await this.loadStatus();
await this.ensureCurrentPreset(status);
};
this.calibrationManager.onCalibrationSet = async () => {
await this.loadStatus();
const status = await this.loadStatus();
await this.ensureCurrentPreset(status);
};
this.referenceManager.onReferenceUpdated = (reference) => {
@ -141,30 +145,91 @@ export class SettingsManager {
async loadInitialData() {
await this.presetManager.loadPresets();
await this.loadStatus();
const status = await this.loadStatus();
await this.calibrationManager.loadWorkingCalibration();
await this.ensureCurrentPreset(status);
}
async loadStatus() {
try {
const status = await apiGet(API.SETTINGS.STATUS);
this.updateStatusDisplay(status);
await this.updateStatusDisplay(status);
return status;
} catch (e) {
console.error('Status load failed:', e);
if (this.elements.systemStatus) {
this.elements.systemStatus.textContent = 'Ошибка';
}
this.notify(ERROR, 'Ошибка статуса', 'Не удалось получить текущее состояние системы');
return null;
}
}
updateStatusDisplay(status) {
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 }),
current_preset: fallbackPreset,
};
if (!status) {
if (this.elements.presetCount) {
this.elements.presetCount.textContent = effectiveStatus.available_presets ?? '-';
}
if (this.elements.calibrationCount) {
this.elements.calibrationCount.textContent = effectiveStatus.available_calibrations ?? '-';
}
}
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);
this.referenceManager.setCurrentPreset(preset);
await 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';
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.systemStatus) {
this.elements.systemStatus.textContent = 'Готово';
}
this.updateHeaderSummary(status);
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
@ -177,7 +242,7 @@ export class SettingsManager {
if (!name || !preset) return;
try {
ButtonState.set(this.elements.viewPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
ButtonState.set(this.elements.viewPlotsBtn, { state: 'loading', icon: 'loader', text: 'Загрузка...' });
const url = buildUrl(API.SETTINGS.CALIBRATION_STANDARDS_PLOTS(name), {
preset_filename: preset.filename
@ -187,9 +252,9 @@ export class SettingsManager {
this.showPlotsModal(plotsData);
} catch (e) {
console.error('Load plots failed:', e);
this.notify(ERROR, 'Plots Error', 'Failed to load calibration plots');
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики калибровки');
} finally {
ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Plots' });
ButtonState.set(this.elements.viewPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Показать графики' });
}
}, TIMING.DEBOUNCE_CALIBRATION);
}
@ -200,19 +265,19 @@ export class SettingsManager {
if (!working || !working.active) return;
try {
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'loading', icon: 'loader', text: 'Loading...' });
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, 'No Data', 'No working calibration or standards available to plot');
this.notify(WARNING, 'Нет данных', 'Нет активной калибровки или стандартов для отображения');
} else {
console.error('Load current plots failed:', e);
this.notify(ERROR, 'Plots Error', 'Failed to load current calibration plots');
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики текущей калибровки');
}
} finally {
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'View Current Plots' });
ButtonState.set(this.elements.viewCurrentPlotsBtn, { state: 'normal', icon: 'bar-chart-3', text: 'Графики текущей калибровки' });
}
}, TIMING.DEBOUNCE_CALIBRATION);
}
@ -228,10 +293,10 @@ export class SettingsManager {
const title = modal.querySelector('.modal__title');
if (title) {
title.innerHTML = `
<i data-lucide="bar-chart-3"></i>
<span data-icon="bar-chart-3"></span>
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
renderIcons(title);
}
this.setupModalCloseHandlers(modal);
@ -247,7 +312,7 @@ export class SettingsManager {
container.innerHTML = '';
if (!individualPlots || !Object.keys(individualPlots).length) {
container.innerHTML = '<div class="plot-error">No calibration plots available</div>';
container.innerHTML = '<div class="plot-error">Нет доступных графиков калибровки</div>';
return;
}
@ -258,7 +323,7 @@ export class SettingsManager {
err.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="alert-circle" class="chart-card__icon"></i>
<span data-icon="alert-circle" class="chart-card__icon"></span>
${name.toUpperCase()} Standard
</div>
</div>
@ -266,6 +331,7 @@ export class SettingsManager {
<div class="plot-error">Error: ${plot.error}</div>
</div>
`;
renderIcons(err);
container.appendChild(err);
return;
}
@ -274,9 +340,7 @@ export class SettingsManager {
container.appendChild(card);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
renderIcons(container);
}
createCalibrationChartCard(standardName, plotConfig, preset) {
@ -284,20 +348,20 @@ export class SettingsManager {
card.className = 'chart-card';
card.dataset.standard = standardName;
const title = `${standardName.toUpperCase()} Standard`;
const title = `Стандарт ${standardName.toUpperCase()}`;
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="bar-chart-3" class="chart-card__icon"></i>
<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="Fullscreen">
<i data-lucide="expand"></i>
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
<span data-icon="expand"></span>
</button>
<button class="chart-card__action" data-action="download" title="Download">
<i data-lucide="download"></i>
<button class="chart-card__action" data-action="download" title="Скачать">
<span data-icon="download"></span>
</button>
</div>
</div>
@ -305,8 +369,8 @@ export class SettingsManager {
<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 class="chart-card__timestamp">Стандарт: ${standardName.toUpperCase()}</div>
<div class="chart-card__sweep">Пресет: ${preset?.filename || 'Неизвестно'}</div>
</div>
`;
@ -322,13 +386,14 @@ export class SettingsManager {
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">Failed to load plot: ${plotConfig?.error || 'Unknown error'}</div>`;
container.innerHTML = `<div class="plot-error">Не удалось загрузить график: ${plotConfig?.error || 'Неизвестная ошибка'}</div>`;
return;
}
@ -415,10 +480,10 @@ export class SettingsManager {
const data = this.prepareCalibrationDownloadData(standardName);
downloadJSON(data, `${base}_data.json`);
this.notify(SUCCESS, 'Download Complete', `Downloaded ${standardName.toUpperCase()} standard plot and data`);
this.notify(SUCCESS, 'Скачивание завершено', `Скачаны график и данные стандарта ${standardName.toUpperCase()}`);
} catch (e) {
console.error('Download standard failed:', e);
this.notify(ERROR, 'Download Failed', 'Failed to download calibration data');
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать данные калибровки');
}
}
@ -431,7 +496,7 @@ export class SettingsManager {
const base = `${calibrationName}_complete_${ts}`;
const btn = this.elements.downloadAllBtn;
if (btn) ButtonState.set(btn, { state: 'loading', icon: 'loader', text: 'Downloading...' });
if (btn) ButtonState.set(btn, { state: 'loading', icon: 'loader', text: 'Скачивание...' });
const complete = {
export_info: {
@ -444,13 +509,13 @@ export class SettingsManager {
downloadJSON(complete, `${base}.json`);
this.notify(SUCCESS, 'Complete Download', `Downloaded complete calibration data for ${calibrationName}`);
this.notify(SUCCESS, 'Полное скачивание завершено', `Полный набор данных калибровки сохранён для ${calibrationName}`);
} catch (e) {
console.error('Download all failed:', e);
this.notify(ERROR, 'Download Failed', 'Failed to download complete calibration data');
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать полный набор данных калибровки');
} finally {
const btn = this.elements.downloadAllBtn;
if (btn) ButtonState.set(btn, { state: 'normal', icon: 'download-cloud', text: 'Download All' });
if (btn) ButtonState.set(btn, { state: 'normal', icon: 'download-cloud', text: 'Скачать всё' });
}
}
@ -491,9 +556,9 @@ export class SettingsManager {
if (!Number.isFinite(numeric)) return null;
const abs = Math.abs(numeric);
const units = [
{ divider: 1e9, suffix: 'GHz' },
{ divider: 1e6, suffix: 'MHz' },
{ divider: 1e3, suffix: 'kHz' }
{ divider: 1e9, suffix: 'ГГц' },
{ divider: 1e6, suffix: 'МГц' },
{ divider: 1e3, suffix: 'кГц' }
];
for (const unit of units) {
@ -504,11 +569,11 @@ export class SettingsManager {
}
}
return `${numeric.toFixed(0)} Hz`;
return `${numeric.toFixed(0)} Гц`;
}
formatPresetSummary(preset) {
if (!preset) return 'Not selected';
if (!preset) return 'Не выбран';
const parts = [];
if (preset.mode) {
@ -524,31 +589,31 @@ export class SettingsManager {
}
if (preset.points) {
parts.push(`${preset.points} pts`);
parts.push(`${preset.points} точек`);
}
if (preset.bandwidth) {
const bw = this.formatFrequency(preset.bandwidth);
if (bw) parts.push(`BW ${bw}`);
if (bw) parts.push(`ПП ${bw}`);
}
return parts.join(' • ') || 'Preset configured';
return parts.join(' • ') || 'Пресет настроен';
}
formatCalibrationSummary(status) {
const active = status?.current_calibration;
if (active?.calibration_name) {
return `Active${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} missing` : '';
return `In progress${working.progress}${missingText}`;
const missingText = missingCount ? ` не хватает ${missingCount} стандартов` : '';
return `В процессе${working.progress}${missingText}`;
}
return 'Not set';
return 'Не задано';
}
updateHeaderSummary(status) {
@ -567,11 +632,11 @@ export class SettingsManager {
if (!target) return;
if (!reference) {
target.textContent = 'Not captured';
target.textContent = 'Не снят';
return;
}
const name = reference.name || 'Reference';
const name = reference.name || 'Эталон';
let timestampText = '';
if (reference.timestamp) {
const timestamp = new Date(reference.timestamp);
@ -580,7 +645,7 @@ export class SettingsManager {
}
}
const summaryParts = ['Active', name];
const summaryParts = ['Активен', name];
if (timestampText) {
summaryParts.push(timestampText);
}

View File

@ -4,6 +4,7 @@
*/
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
import { renderIcons } from '../icons.js';
import { apiGet, apiPost, buildUrl } from '../api-client.js';
import { API, TIMING, CALIBRATION_STANDARDS, NOTIFICATION_TYPES } from '../constants.js';
@ -86,14 +87,14 @@ export class CalibrationManager {
dd.innerHTML = '';
if (!calibrations.length) {
dd.innerHTML = '<option value="">No calibrations available</option>';
dd.innerHTML = '<option value="">Калибровки отсутствуют</option>';
dd.disabled = true;
this.elements.setCalibrationBtn.disabled = true;
this.elements.viewPlotsBtn.disabled = true;
return;
}
dd.innerHTML = '<option value="">Select calibration...</option>';
dd.innerHTML = '<option value="">Выберите калибровку...</option>';
calibrations.forEach(c => {
const opt = document.createElement('option');
opt.value = c.name;
@ -158,19 +159,19 @@ export class CalibrationManager {
if (capturing) {
btn.classList.add('btn--warning');
btn.innerHTML = `<i data-lucide="clock"></i> Capturing ${std.toUpperCase()}...`;
btn.innerHTML = `<span data-icon="clock"></span> Снятие ${std.toUpperCase()}...`;
btn.disabled = true;
btn.title = 'Standard is currently being captured';
btn.title = 'Стандарт сейчас снимается';
} else if (isCompleted) {
btn.classList.add('btn--success');
btn.innerHTML = `<i data-lucide="check"></i> ${std.toUpperCase()}`;
btn.innerHTML = `<span data-icon="check"></span> ${std.toUpperCase()}`;
btn.disabled = false;
btn.title = 'Click to recapture this standard';
btn.title = 'Нажмите, чтобы переснять стандарт';
} else if (isMissing) {
btn.classList.add('btn--primary');
btn.innerHTML = `<i data-lucide="radio"></i> Capture ${std.toUpperCase()}`;
btn.innerHTML = `<span data-icon="radio"></span> Снять ${std.toUpperCase()}`;
btn.disabled = false;
btn.title = 'Click to capture this standard';
btn.title = 'Нажмите, чтобы снять стандарт';
} else {
btn.classList.add('btn--secondary');
btn.innerHTML = `${std.toUpperCase()}`;
@ -181,9 +182,7 @@ export class CalibrationManager {
container.appendChild(btn);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
renderIcons(container);
}
getStandardsForMode() {
@ -204,20 +203,20 @@ export class CalibrationManager {
this.debouncer.debounce('start-calibration', () =>
this.reqGuard.runExclusive('start-calibration', async () => {
try {
ButtonState.set(this.elements.startCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Starting...' });
ButtonState.set(this.elements.startCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Запуск...' });
const result = await apiPost(API.SETTINGS.CALIBRATION_START, {
preset_filename: this.currentPreset.filename
});
this.notify(INFO, 'Calibration Started', `Started calibration for ${result.preset}`);
this.notify(INFO, 'Калибровка запущена', `Запущена калибровка для ${result.preset}`);
await this.loadWorkingCalibration();
} catch (e) {
console.error('Start calibration failed:', e);
this.notify(ERROR, 'Calibration Error', 'Failed to start calibration');
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось запустить калибровку');
} finally {
ButtonState.set(this.elements.startCalibrationBtn, { state: 'normal', icon: 'play', text: 'Start Calibration' });
ButtonState.set(this.elements.startCalibrationBtn, { state: 'normal', icon: 'play', text: 'Начать калибровку' });
}
}), TIMING.DEBOUNCE_CALIBRATION
);
@ -233,19 +232,19 @@ export class CalibrationManager {
this.disabledStandards.add(standard);
const btn = document.querySelector(`[data-standard="${standard}"]`);
ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Capturing...' });
ButtonState.set(btn, { state: 'loading', icon: 'upload', text: 'Снятие...' });
this.notify(INFO, 'Capturing Standard', `Capturing ${standard.toUpperCase()} standard...`);
this.notify(INFO, 'Снятие стандарта', `Снимается стандарт ${standard.toUpperCase()}...`);
const result = await apiPost(API.SETTINGS.CALIBRATION_ADD_STANDARD, { standard });
this.notify(SUCCESS, 'Standard Captured', result.message);
this.notify(SUCCESS, 'Стандарт снят', result.message);
this.resetCaptureState();
await this.loadWorkingCalibration();
} catch (e) {
console.error('Capture standard failed:', e);
this.notify(ERROR, 'Calibration Error', 'Failed to capture calibration standard');
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось снять стандарт калибровки');
this.resetCaptureState(standard);
}
}), TIMING.DEBOUNCE_CALIBRATION
@ -259,11 +258,11 @@ export class CalibrationManager {
this.debouncer.debounce('save-calibration', () =>
this.reqGuard.runExclusive('save-calibration', async () => {
try {
ButtonState.set(this.elements.saveCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Saving...' });
ButtonState.set(this.elements.saveCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Сохранение...' });
const result = await apiPost(API.SETTINGS.CALIBRATION_SAVE, { name });
this.notify(SUCCESS, 'Calibration Saved', result.message);
this.notify(SUCCESS, 'Калибровка сохранена', result.message);
this.hideSteps();
this.elements.calibrationNameInput.value = '';
@ -276,9 +275,9 @@ export class CalibrationManager {
}
} catch (e) {
console.error('Save calibration failed:', e);
this.notify(ERROR, 'Calibration Error', 'Failed to save calibration');
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось сохранить калибровку');
} finally {
ButtonState.set(this.elements.saveCalibrationBtn, { state: 'disabled', icon: 'save', text: 'Save Calibration' });
ButtonState.set(this.elements.saveCalibrationBtn, { state: 'disabled', icon: 'save', text: 'Сохранить калибровку' });
}
}), TIMING.DEBOUNCE_CALIBRATION
);
@ -291,23 +290,23 @@ export class CalibrationManager {
if (!name || !this.currentPreset) return;
try {
ButtonState.set(this.elements.setCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
ButtonState.set(this.elements.setCalibrationBtn, { state: 'loading', icon: 'loader', text: 'Применение...' });
const result = await apiPost(API.SETTINGS.CALIBRATION_SET, {
name,
preset_filename: this.currentPreset.filename
});
this.notify(SUCCESS, 'Calibration Set', result.message);
this.notify(SUCCESS, 'Калибровка применена', result.message);
if (this.onCalibrationSet) {
await this.onCalibrationSet();
}
} catch (e) {
console.error('Set calibration failed:', e);
this.notify(ERROR, 'Calibration Error', 'Failed to set active calibration');
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось применить калибровку');
} finally {
ButtonState.set(this.elements.setCalibrationBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
ButtonState.set(this.elements.setCalibrationBtn, { state: 'normal', icon: 'check', text: 'Сделать активной' });
}
}), TIMING.DEBOUNCE_CALIBRATION
);
@ -319,7 +318,7 @@ export class CalibrationManager {
this.elements.currentCalibration.textContent = status.current_calibration.calibration_name;
} else {
this.currentCalibration = null;
this.elements.currentCalibration.textContent = 'None';
this.elements.currentCalibration.textContent = 'Нет';
}
}

View File

@ -38,7 +38,7 @@ export class PresetManager {
this.populateDropdown(presets);
} catch (e) {
console.error('Presets load failed:', e);
this.notify(ERROR, 'Load Error', 'Failed to load configuration presets');
this.notify(ERROR, 'Ошибка загрузки', 'Не удалось загрузить пресеты конфигурации');
}
}
@ -47,7 +47,7 @@ export class PresetManager {
dd.innerHTML = '';
if (!presets.length) {
dd.innerHTML = '<option value="">No presets available</option>';
dd.innerHTML = '<option value="">Пресеты отсутствуют</option>';
dd.disabled = true;
if (this.elements.setPresetBtn) {
this.elements.setPresetBtn.disabled = true;
@ -55,7 +55,7 @@ export class PresetManager {
return;
}
dd.innerHTML = '<option value="">Select preset...</option>';
dd.innerHTML = '<option value="">Выберите пресет...</option>';
presets.forEach(p => {
const opt = document.createElement('option');
opt.value = p.filename;
@ -89,11 +89,11 @@ export class PresetManager {
this.debouncer.debounce('set-preset', () =>
this.reqGuard.runExclusive('set-preset', async () => {
try {
ButtonState.set(this.elements.setPresetBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
ButtonState.set(this.elements.setPresetBtn, { state: 'loading', icon: 'loader', text: 'Применение...' });
const result = await apiPost(API.SETTINGS.PRESET_SET, { filename });
this.notify(SUCCESS, 'Preset Set', result.message);
this.notify(SUCCESS, 'Пресет применён', result.message);
this.currentPreset = { filename };
if (this.onPresetChanged) {
@ -102,9 +102,9 @@ export class PresetManager {
this.syncSelectedPreset();
} catch (e) {
console.error('Set preset failed:', e);
this.notify(ERROR, 'Preset Error', 'Failed to set configuration preset');
this.notify(ERROR, 'Ошибка пресета', 'Не удалось применить пресет');
} finally {
ButtonState.set(this.elements.setPresetBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
ButtonState.set(this.elements.setPresetBtn, { state: 'normal', icon: 'check', text: 'Сделать активным' });
this.updateSetButtonState();
}
}), TIMING.DEBOUNCE_PRESET
@ -117,7 +117,7 @@ export class PresetManager {
this.elements.currentPreset.textContent = status.current_preset.filename;
} else {
this.currentPreset = null;
this.elements.currentPreset.textContent = 'None';
this.elements.currentPreset.textContent = 'Нет';
}
this.syncSelectedPreset();
}
@ -126,6 +126,14 @@ export class PresetManager {
return this.currentPreset;
}
setCurrentPresetDirect(preset) {
this.currentPreset = preset;
if (this.elements.currentPreset) {
this.elements.currentPreset.textContent = preset ? preset.filename : 'Нет';
}
this.syncSelectedPreset();
}
syncSelectedPreset() {
const dd = this.elements.presetDropdown;
if (!dd) return;

View File

@ -44,10 +44,10 @@ export class ReferenceManager {
this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference);
}
setCurrentPreset(preset) {
async setCurrentPreset(preset) {
this.currentPreset = preset;
if (preset) {
this.loadReferences();
await this.loadReferences();
} else {
this.reset();
}
@ -89,14 +89,18 @@ export class ReferenceManager {
this.elements.referenceDropdown.innerHTML = '';
const setBtn = this.elements.setReferenceBtn;
const deleteBtn = this.elements.deleteReferenceBtn;
const clearBtn = this.elements.clearReferenceBtn;
if (references.length === 0) {
this.elements.referenceDropdown.innerHTML = '<option value="">No references available</option>';
this.elements.referenceDropdown.innerHTML = '<option value="">Эталоны отсутствуют</option>';
this.elements.referenceDropdown.disabled = true;
this.elements.setReferenceBtn.disabled = true;
this.elements.clearReferenceBtn.disabled = true;
this.elements.deleteReferenceBtn.disabled = true;
if (setBtn) setBtn.disabled = true;
if (clearBtn) clearBtn.disabled = true;
if (deleteBtn) deleteBtn.disabled = true;
} else {
this.elements.referenceDropdown.innerHTML = '<option value="">Select a reference...</option>';
this.elements.referenceDropdown.innerHTML = '<option value="">Выберите эталон...</option>';
references.forEach(ref => {
const option = document.createElement('option');
@ -110,18 +114,28 @@ export class ReferenceManager {
if (this.currentReference) {
this.elements.referenceDropdown.value = this.currentReference.name;
}
if (setBtn) setBtn.disabled = false;
if (deleteBtn) deleteBtn.disabled = false;
}
this.updateButtons();
}
updateButtons() {
const hasSelection = this.elements.referenceDropdown.value !== '';
const dropdown = this.elements.referenceDropdown;
if (!dropdown) return;
const hasSelection = dropdown.value !== '';
const hasCurrent = this.currentReference !== null;
this.elements.setReferenceBtn.disabled = !hasSelection;
this.elements.deleteReferenceBtn.disabled = !hasSelection;
this.elements.clearReferenceBtn.disabled = !hasCurrent;
const setBtn = this.elements.setReferenceBtn;
const deleteBtn = this.elements.deleteReferenceBtn;
const clearBtn = this.elements.clearReferenceBtn;
if (setBtn) setBtn.disabled = !hasSelection;
if (deleteBtn) deleteBtn.disabled = !hasSelection;
if (clearBtn) clearBtn.disabled = !hasCurrent;
}
updateInfo(reference) {
@ -161,7 +175,7 @@ export class ReferenceManager {
async handleCreateReference() {
const name = this.elements.referenceNameInput.value.trim();
if (!name) {
this.notify(WARNING, 'Missing Name', 'Please enter a name for the reference');
this.notify(WARNING, 'Нет имени', 'Введите имя эталона');
return;
}
@ -170,11 +184,11 @@ export class ReferenceManager {
this.debouncer.debounce('create-reference', () =>
this.reqGuard.runExclusive('create-reference', async () => {
try {
ButtonState.set(this.elements.createReferenceBtn, { state: 'loading', icon: 'loader', text: 'Creating...' });
ButtonState.set(this.elements.createReferenceBtn, { state: 'loading', icon: 'loader', text: 'Создание...' });
const result = await apiPost(API.SETTINGS.REFERENCE_CREATE, { name, description });
this.notify(SUCCESS, 'Reference Created', result.message);
this.notify(SUCCESS, 'Эталон создан', result.message);
this.elements.referenceNameInput.value = '';
this.elements.referenceDescriptionInput.value = '';
@ -182,9 +196,9 @@ export class ReferenceManager {
await this.loadReferences();
} catch (error) {
console.error('Create reference failed:', error);
this.notify(ERROR, 'Reference Error', 'Failed to create reference');
this.notify(ERROR, 'Ошибка эталона', 'Не удалось создать эталон');
} finally {
ButtonState.set(this.elements.createReferenceBtn, { state: 'normal', icon: 'target', text: 'Capture Reference' });
ButtonState.set(this.elements.createReferenceBtn, { state: 'normal', icon: 'target', text: 'Снять эталон' });
}
}), TIMING.DEBOUNCE_REFERENCE
);
@ -201,18 +215,18 @@ export class ReferenceManager {
this.debouncer.debounce('set-reference', () =>
this.reqGuard.runExclusive('set-reference', async () => {
try {
ButtonState.set(this.elements.setReferenceBtn, { state: 'loading', icon: 'loader', text: 'Setting...' });
ButtonState.set(this.elements.setReferenceBtn, { state: 'loading', icon: 'loader', text: 'Применение...' });
const result = await apiPost(API.SETTINGS.REFERENCE_SET, { name: referenceName });
this.notify(SUCCESS, 'Reference Set', result.message);
this.notify(SUCCESS, 'Эталон применён', result.message);
await this.loadReferences();
} catch (error) {
console.error('Set reference failed:', error);
this.notify(ERROR, 'Reference Error', 'Failed to set reference');
this.notify(ERROR, 'Ошибка эталона', 'Не удалось применить эталон');
} finally {
ButtonState.set(this.elements.setReferenceBtn, { state: 'normal', icon: 'check', text: 'Set Active' });
ButtonState.set(this.elements.setReferenceBtn, { state: 'normal', icon: 'check', text: 'Сделать активным' });
}
}), TIMING.DEBOUNCE_REFERENCE
);
@ -222,18 +236,24 @@ export class ReferenceManager {
this.debouncer.debounce('clear-reference', () =>
this.reqGuard.runExclusive('clear-reference', async () => {
try {
ButtonState.set(this.elements.clearReferenceBtn, { state: 'loading', icon: 'loader', text: 'Clearing...' });
const clearBtn = this.elements.clearReferenceBtn;
if (clearBtn) {
ButtonState.set(clearBtn, { state: 'loading', icon: 'loader', text: 'Очистка...' });
}
const result = await apiDelete(API.SETTINGS.REFERENCE_CURRENT);
this.notify(SUCCESS, 'Reference Cleared', result.message);
this.notify(SUCCESS, 'Эталон сброшен', result.message);
await this.loadReferences();
} catch (error) {
console.error('Clear reference failed:', error);
this.notify(ERROR, 'Reference Error', 'Failed to clear reference');
this.notify(ERROR, 'Ошибка эталона', 'Не удалось сбросить эталон');
} finally {
ButtonState.set(this.elements.clearReferenceBtn, { state: 'normal', icon: 'x', text: 'Clear' });
const clearBtn = this.elements.clearReferenceBtn;
if (clearBtn) {
ButtonState.set(clearBtn, { state: 'normal', icon: 'x', text: 'Сбросить' });
}
}
}), TIMING.DEBOUNCE_REFERENCE
);
@ -250,18 +270,18 @@ export class ReferenceManager {
this.debouncer.debounce('delete-reference', () =>
this.reqGuard.runExclusive('delete-reference', async () => {
try {
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'loading', icon: 'loader', text: 'Deleting...' });
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'loading', icon: 'loader', text: 'Удаление...' });
const result = await apiDelete(API.SETTINGS.REFERENCE_ITEM(referenceName));
this.notify(SUCCESS, 'Reference Deleted', result.message);
this.notify(SUCCESS, 'Эталон удалён', result.message);
await this.loadReferences();
} catch (error) {
console.error('Delete reference failed:', error);
this.notify(ERROR, 'Reference Error', 'Failed to delete reference');
this.notify(ERROR, 'Ошибка эталона', 'Не удалось удалить эталон');
} finally {
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'normal', icon: 'trash-2', text: 'Delete' });
ButtonState.set(this.elements.deleteReferenceBtn, { state: 'normal', icon: 'trash-2', text: 'Удалить' });
}
}), TIMING.DEBOUNCE_REFERENCE
);

View File

@ -4,6 +4,7 @@
*/
import { formatProcessorName, debounce } from './utils.js';
import { renderIcons } from './icons.js';
export class UIManager {
constructor(notifications, websocket, charts) {
@ -157,16 +158,16 @@ export class UIManager {
switch (status) {
case 'connected':
statusElement.classList.add('status-indicator--connected');
textElement.textContent = 'Connected';
textElement.textContent = 'Подключено';
break;
case 'connecting':
statusElement.classList.add('status-indicator--connecting');
textElement.textContent = 'Connecting...';
textElement.textContent = 'Подключение...';
break;
case 'disconnected':
default:
statusElement.classList.add('status-indicator--disconnected');
textElement.textContent = 'Disconnected';
textElement.textContent = 'Отключено';
break;
}
}
@ -218,9 +219,7 @@ export class UIManager {
container.appendChild(toggle);
}
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
renderIcons(container);
}
/**

View File

@ -1,7 +1,4 @@
/**
* Shared Utility Functions
* Common utilities used across the application
*/
import { renderIcons } from './icons.js';
/**
* Format processor name: convert snake_case to Title Case
@ -100,22 +97,20 @@ export class ButtonState {
switch (state) {
case 'loading':
element.disabled = true;
element.innerHTML = `<i data-lucide="${icon || 'loader'}"></i> ${text || 'Loading...'}`;
element.innerHTML = `<span data-icon="${icon || 'loader'}"></span> ${text || 'Загрузка...'}`;
break;
case 'disabled':
element.disabled = true;
element.innerHTML = icon ? `<i data-lucide="${icon}"></i> ${text}` : text;
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
break;
case 'normal':
default:
element.disabled = !!disabled;
element.innerHTML = icon ? `<i data-lucide="${icon}"></i> ${text}` : text;
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
break;
}
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
renderIcons(element);
}
}
@ -206,7 +201,7 @@ export function createParameterControl(param, processorId, idPrefix = 'param') {
}
case 'button':
const buttonText = param.label || 'Click';
const buttonText = param.label || 'Нажать';
const actionDesc = opts.action ? `title="${opts.action}"` : '';
return `
<div class="chart-setting" data-param="${param.name}">
@ -246,7 +241,7 @@ export function createParameterControl(param, processorId, idPrefix = 'param') {
*/
export function safeClone(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return '[Circular Reference]';
if (seen.has(obj)) return '[Циклическая ссылка]';
seen.add(obj);
@ -260,7 +255,7 @@ export function safeClone(obj, seen = new WeakSet()) {
try {
cloned[key] = safeClone(obj[key], seen);
} catch (e) {
cloned[key] = `[Error: ${e.message}]`;
cloned[key] = `[Ошибка: ${e.message}]`;
}
}
}
@ -308,11 +303,11 @@ export function escapeHtml(unsafe) {
* @returns {string} Formatted string
*/
export function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
if (bytes === 0) return '0 байт';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const sizes = ['байт', 'КБ', 'МБ', 'ГБ'];
const i = Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}

View File

@ -97,8 +97,8 @@ export class WebSocketManager {
this.notifications?.show?.({
type: 'success',
title: 'Connected',
message: 'Real-time connection established'
title: 'Подключено',
message: 'Установлено соединение в реальном времени'
});
};
@ -109,8 +109,8 @@ export class WebSocketManager {
console.error('Error processing WebSocket message:', error);
this.notifications?.show?.({
type: 'error',
title: 'Message Error',
message: 'Failed to process received data'
title: 'Ошибка сообщения',
message: 'Не удалось обработать полученные данные'
});
}
};
@ -148,8 +148,8 @@ export class WebSocketManager {
console.error('JSON parse error:', jsonError);
this.notifications?.show?.({
type: 'error',
title: 'JSON Parse Error',
message: `Failed to parse JSON: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
title: 'Ошибка разбора JSON',
message: `Не удалось разобрать JSON: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
});
return;
}
@ -175,8 +175,8 @@ export class WebSocketManager {
});
this.notifications?.show?.({
type: 'error',
title: 'Server Error',
message: `${payload.message}${payload.details ? ` - ${payload.details}` : ''}${payload.source ? ` (${payload.source})` : ''}`
title: 'Ошибка сервера',
message: `${payload.message}${payload.details ? ` ${payload.details}` : ''}${payload.source ? ` (${payload.source})` : ''}`
});
break;
default:
@ -187,8 +187,8 @@ export class WebSocketManager {
console.log('Raw message:', data);
this.notifications?.show?.({
type: 'error',
title: 'Message Processing Error',
message: `Error processing message: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
title: 'Ошибка обработки сообщения',
message: `Не удалось обработать сообщение: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
});
}
}
@ -223,8 +223,8 @@ export class WebSocketManager {
// Only show error notification during connection attempts, not after disconnection
this.notifications?.show?.({
type: 'error',
title: 'Connection Failed',
message: 'Unable to establish real-time connection'
title: 'Сбой подключения',
message: 'Не удалось установить соединение в реальном времени'
});
}
}
@ -241,8 +241,8 @@ export class WebSocketManager {
if (wasConnected) {
this.notifications?.show?.({
type: 'warning',
title: 'Disconnected',
message: 'Real-time connection lost. Attempting to reconnect...'
title: 'Отключено',
message: 'Соединение потеряно. Пытаемся переподключиться...'
});
this.scheduleReconnect();
}
@ -263,8 +263,8 @@ export class WebSocketManager {
console.error('Max reconnection attempts reached');
this.notifications?.show?.({
type: 'error',
title: 'Connection Failed',
message: 'Unable to reconnect after multiple attempts'
title: 'Сбой подключения',
message: 'Не удалось переподключиться после нескольких попыток'
});
return;
}

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Real-time VNA (Vector Network Analyzer) data acquisition and processing dashboard">
<meta name="keywords" content="VNA, Vector Network Analyzer, RF, Microwave, Data Acquisition, Real-time">
<meta name="description" content="Панель управления системой ВНА для сбора и обработки данных в реальном времени">
<meta name="keywords" content="ВНА, Vector Network Analyzer, РФ, микроволны, сбор данных, реальное время">
<meta name="author" content="VNA System">
<title>VNA System Dashboard</title>
<title>Система ВНА — Панель управления</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/static/assets/favicon.svg">
@ -15,15 +15,8 @@
<!-- Theme color for mobile browsers -->
<meta name="theme-color" content="#1e293b">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Icons will be loaded with JavaScript -->
<!-- Plotly.js -->
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
<script src="/static/js/vendor/plotly-2.26.0.min.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/static/css/normalize.css">
@ -39,42 +32,25 @@
<header class="header">
<div class="header__container">
<div class="header__brand">
<i data-lucide="activity" class="header__icon"></i>
<h1 class="header__title">VNA System</h1>
<div class="header__summary" id="headerSummary">
<div class="header-summary__item">
<span class="header-summary__label">Preset</span>
<span class="header-summary__value" id="headerPresetSummary">Not selected</span>
</div>
<span class="header-summary__divider" aria-hidden="true"></span>
<div class="header-summary__item">
<span class="header-summary__label">Calibration</span>
<span class="header-summary__value" id="headerCalibrationSummary">Not set</span>
</div>
<span class="header-summary__divider" aria-hidden="true"></span>
<div class="header-summary__item">
<span class="header-summary__label">Reference</span>
<span class="header-summary__value" id="headerReferenceSummary">Not captured</span>
</div>
</div>
<span data-icon="activity" class="header__icon"></span>
<h1 class="header__title">МФТИ</h1>
</div>
<div class="header__status">
<div class="status-indicator" id="connectionStatus">
<div class="status-indicator__dot"></div>
<span class="status-indicator__text">Connecting...</span>
<span class="status-indicator__text">Подключение...</span>
</div>
</div>
<nav class="header__nav">
<button class="nav-btn nav-btn--active" data-view="dashboard">
<i data-lucide="bar-chart-3"></i>
<span>Dashboard</span>
<span data-icon="bar-chart-3"></span>
<span>Панель</span>
</button>
<button class="nav-btn" data-view="settings">
<i data-lucide="settings"></i>
<span>Settings</span>
<span data-icon="settings"></span>
<span>Настройки</span>
</button>
</nav>
</div>
@ -88,55 +64,72 @@
<div class="controls-panel">
<div class="controls-panel__container">
<div class="controls-group">
<label class="controls-label">Acquisition Control</label>
<label class="controls-label">Управление сбором</label>
<div class="acquisition-controls">
<button class="btn btn--primary btn--bordered" id="startBtn" title="Start continuous acquisition">
<i data-lucide="play"></i>
Start
</button>
<button class="btn btn--secondary btn--bordered" id="stopBtn" title="Stop acquisition">
<i data-lucide="square"></i>
Stop
</button>
<button class="btn btn--accent btn--bordered" id="singleSweepBtn" title="Trigger single sweep">
<i data-lucide="zap"></i>
Single
</button>
<div class="acquisition-controls__buttons">
<button class="btn btn--primary" id="startBtn" title="Запустить непрерывный сбор">
<span data-icon="play"></span>
Запуск
</button>
<button class="btn btn--secondary" id="stopBtn" title="Остановить сбор">
<span data-icon="square"></span>
Стоп
</button>
<button class="btn btn--accent" id="singleSweepBtn" title="Запустить одиночный свип">
<span data-icon="zap"></span>
Одиночный
</button>
</div>
<div class="acquisition-summary header__summary" id="headerSummary">
<div class="header-summary__item">
<span class="header-summary__label">Пресет</span>
<span class="header-summary__value" id="headerPresetSummary">Не выбран</span>
</div>
<span class="header-summary__divider" aria-hidden="true"></span>
<div class="header-summary__item">
<span class="header-summary__label">Калибровка</span>
<span class="header-summary__value" id="headerCalibrationSummary">Не задана</span>
</div>
<span class="header-summary__divider" aria-hidden="true"></span>
<div class="header-summary__item">
<span class="header-summary__label">Эталон</span>
<span class="header-summary__value" id="headerReferenceSummary">Не снят</span>
</div>
</div>
</div>
<div class="acquisition-status" id="acquisitionStatus">
<div class="status-indicator">
<div class="status-indicator__dot status-indicator__dot--idle"></div>
<span class="status-indicator__text" id="acquisitionStatusText">Idle</span>
<span class="status-indicator__text" id="acquisitionStatusText">Ожидание</span>
</div>
<div class="acquisition-mode" id="acquisitionMode">
<span class="mode-label">Mode:</span>
<span class="mode-value" id="acquisitionModeText">Continuous</span>
<span class="mode-label">Режим:</span>
<span class="mode-value" id="acquisitionModeText">Непрерывный</span>
</div>
</div>
</div>
<div class="controls-group">
<label class="controls-label">Processors</label>
<label class="controls-label">Процессоры</label>
<div class="processor-toggles" id="processorToggles">
<!-- Processor toggles will be dynamically generated -->
<!-- Переключатели процессоров создаются динамически -->
</div>
</div>
</div>
</div>
<!-- Charts Grid -->
<div class="charts-container">
<div class="charts-grid" id="chartsGrid">
<!-- Charts will be dynamically generated -->
<!-- Графики создаются динамически -->
</div>
<div class="empty-state" id="emptyState">
<div class="empty-state__content">
<i data-lucide="activity" class="empty-state__icon"></i>
<h3 class="empty-state__title">No Data Yet</h3>
<span data-icon="activity" class="empty-state__icon"></span>
<h3 class="empty-state__title">Нет данных</h3>
<p class="empty-state__description">
Waiting for sweep data from the VNA system...
Ожидаем данные свипа от системы ВНА...
</p>
<div class="loading-spinner">
<div class="spinner"></div>
@ -151,25 +144,25 @@
<div class="settings-container">
<div class="settings-section">
<h2 class="settings-title">
<i data-lucide="settings" class="settings-icon"></i>
VNA Settings
<span data-icon="settings" class="settings-icon"></span>
Настройки системы
</h2>
<!-- Configuration Presets Section -->
<div class="settings-card">
<h3 class="settings-card-title">Configuration Presets</h3>
<p class="settings-card-description">Select measurement configuration from available presets</p>
<h3 class="settings-card-title">Конфигурационные пресеты</h3>
<p class="settings-card-description">Выберите конфигурацию измерений из доступных пресетов</p>
<div class="preset-controls">
<div class="control-group">
<label class="control-label">Available Presets:</label>
<label class="control-label">Доступные пресеты:</label>
<div class="preset-selector" id="presetSelector">
<select class="preset-dropdown settings-select" id="presetDropdown" disabled>
<option value="">Loading presets...</option>
<option value="">Загрузка пресетов...</option>
</select>
<button class="btn btn--primary" id="setPresetBtn" disabled>
<i data-lucide="check"></i>
Set Active
<span data-icon="check"></span>
Сделать активным
</button>
</div>
</div>
@ -177,8 +170,8 @@
<div class="preset-info" id="presetInfo">
<div class="preset-details">
<div class="preset-param">
<span class="param-label">Current:</span>
<span class="param-value" id="currentPreset">None</span>
<span class="param-label">Текущий:</span>
<span class="param-value" id="currentPreset">Нет</span>
</div>
</div>
</div>
@ -187,14 +180,14 @@
<!-- Calibration Section -->
<div class="settings-card">
<h3 class="settings-card-title">Calibration Management</h3>
<p class="settings-card-description">Manage calibration data for accurate measurements</p>
<h3 class="settings-card-title">Управление калибровкой</h3>
<p class="settings-card-description">Управление калибровочными данными для точных измерений</p>
<!-- Current Calibration Status -->
<div class="calibration-status" id="calibrationStatus">
<div class="status-item">
<span class="status-label">Current Calibration:</span>
<span class="status-value" id="currentCalibration">None</span>
<span class="status-label">Текущая калибровка:</span>
<span class="status-value" id="currentCalibration">Нет</span>
</div>
</div>
@ -202,18 +195,18 @@
<div class="calibration-workflow">
<!-- Start New Calibration -->
<div class="workflow-section">
<h4 class="workflow-title">New Calibration</h4>
<h4 class="workflow-title">Новая калибровка</h4>
<div class="workflow-controls">
<button class="btn btn--primary" id="startCalibrationBtn" disabled>
<i data-lucide="play"></i>
Start Calibration
<span data-icon="play"></span>
Начать калибровку
</button>
</div>
</div>
<!-- Calibration Steps -->
<div class="workflow-section" id="calibrationSteps" style="display: none;">
<h4 class="workflow-title">Calibration Steps</h4>
<h4 class="workflow-title">Шаги калибровки</h4>
<div class="calibration-progress" id="calibrationProgress">
<div class="progress-info">
<span class="progress-text" id="progressText">0/0</span>
@ -221,37 +214,37 @@
</div>
<div class="calibration-standards" id="calibrationStandards">
<!-- Standards buttons will be dynamically generated -->
<!-- Кнопки стандартов создаются динамически -->
</div>
<div class="calibration-actions settings-action-group">
<button class="btn btn--secondary" id="viewCurrentPlotsBtn" disabled>
<i data-lucide="bar-chart-3"></i>
View Current Plots
<span data-icon="bar-chart-3"></span>
Графики текущей калибровки
</button>
<input type="text" class="calibration-name-input settings-input" id="calibrationNameInput" placeholder="Enter calibration name" disabled>
<input type="text" class="calibration-name-input settings-input" id="calibrationNameInput" placeholder="Введите имя калибровки" disabled>
<button class="btn btn--success" id="saveCalibrationBtn" disabled>
<i data-lucide="save"></i>
Save Calibration
<span data-icon="save"></span>
Сохранить калибровку
</button>
</div>
</div>
<!-- Existing Calibrations -->
<div class="workflow-section">
<h4 class="workflow-title">Existing Calibrations</h4>
<h4 class="workflow-title">Существующие калибровки</h4>
<div class="existing-calibrations" id="existingCalibrations">
<select class="calibration-dropdown settings-select" id="calibrationDropdown" disabled>
<option value="">No calibrations available</option>
<option value="">Калибровки отсутствуют</option>
</select>
<div class="calibration-actions settings-action-group">
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
<i data-lucide="check"></i>
Set Active
<span data-icon="check"></span>
Сделать активной
</button>
<button class="btn btn--secondary" id="viewPlotsBtn" disabled>
<i data-lucide="bar-chart-3"></i>
View Plots
<span data-icon="bar-chart-3"></span>
Показать графики
</button>
</div>
</div>
@ -261,75 +254,71 @@
<!-- Open Air Reference -->
<div class="settings-card">
<h3 class="settings-card-title">Open Air Reference</h3>
<h3 class="settings-card-title">Эталон открытого пространства</h3>
<div class="settings-card-content">
<!-- Create Reference -->
<div class="workflow-section">
<h4 class="workflow-title">Create Reference</h4>
<h4 class="workflow-title">Создать эталон</h4>
<div class="reference-creation">
<input type="text" class="reference-name-input settings-input" id="referenceNameInput" placeholder="Enter reference name">
<input type="text" class="reference-description-input settings-input" id="referenceDescriptionInput" placeholder="Description (optional)">
<input type="text" class="reference-name-input settings-input" id="referenceNameInput" placeholder="Введите имя эталона">
<input type="text" class="reference-description-input settings-input" id="referenceDescriptionInput" placeholder="Описание (необязательно)">
<button class="btn btn--primary" id="createReferenceBtn">
<i data-lucide="target"></i>
Capture Reference
<span data-icon="target"></span>
Снять эталон
</button>
</div>
</div>
<!-- Existing References -->
<div class="workflow-section">
<h4 class="workflow-title">Existing References</h4>
<div class="existing-references" id="existingReferences">
<h4 class="workflow-title">Существующие эталоны</h4>
<div class="reference-list" id="referenceList">
<select class="reference-dropdown settings-select" id="referenceDropdown" disabled>
<option value="">No references available</option>
<option value="">Эталоны отсутствуют</option>
</select>
<div class="reference-actions settings-action-group">
<button class="btn btn--primary" id="setReferenceBtn" disabled>
<i data-lucide="check"></i>
Set Active
<span data-icon="check"></span>
Использовать
</button>
<button class="btn btn--secondary" id="clearReferenceBtn" disabled>
<i data-lucide="x"></i>
Clear
<button class="btn btn--secondary" id="previewReferenceBtn" disabled>
<span data-icon="bar-chart-3"></span>
Просмотр
</button>
<button class="btn btn--danger" id="deleteReferenceBtn" disabled>
<i data-lucide="trash-2"></i>
Delete
<button class="btn btn--secondary" id="deleteReferenceBtn" disabled>
<span data-icon="trash-2"></span>
Удалить
</button>
</div>
</div>
<!-- Current Reference Info -->
<div class="current-reference-info" id="currentReferenceInfo" style="display: none;">
<div class="reference-info-card">
<h5>Current Reference</h5>
<div class="reference-details">
<span class="reference-name" id="currentReferenceName">-</span>
<span class="reference-timestamp" id="currentReferenceTimestamp">-</span>
<p class="reference-description" id="currentReferenceDescription">-</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Status Summary -->
<!-- System Summary -->
<div class="settings-card">
<h3 class="settings-card-title">Status Summary</h3>
<div class="status-grid" id="statusSummary">
<h3 class="settings-card-title">Сводка системы</h3>
<div class="system-summary" id="systemSummary">
<div class="status-item">
<span class="status-label">Available Presets:</span>
<span class="status-value" id="presetCount">-</span>
<span class="status-label">Получено свипов:</span>
<span class="status-value" id="sweepCount">-</span>
</div>
<div class="status-item">
<span class="status-label">Available Calibrations:</span>
<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">
<span class="status-label">Калибровок сохранено:</span>
<span class="status-value" id="calibrationCount">-</span>
</div>
<div class="status-item">
<span class="status-label">System Status:</span>
<span class="status-value" id="systemStatus">Checking...</span>
<span class="status-label">Статус системы:</span>
<span class="status-value" id="systemStatus">Проверка...</span>
</div>
</div>
</div>
@ -344,16 +333,16 @@
<div class="modal__content modal__content--large">
<div class="modal__header">
<h2 class="modal__title">
<i data-lucide="bar-chart-3"></i>
Calibration Standards Plots
<span data-icon="bar-chart-3"></span>
Графики стандартов калибровки
</h2>
<div class="modal__actions">
<button class="btn btn--secondary btn--sm" id="downloadAllBtn" title="Download all calibration data">
<i data-lucide="download-cloud"></i>
Download All
<button class="btn btn--secondary btn--sm" id="downloadAllBtn" title="Скачать все данные калибровки">
<span data-icon="download-cloud"></span>
Скачать всё
</button>
<button class="modal__close" data-modal-close title="Close">
<i data-lucide="x"></i>
<button class="modal__close" data-modal-close title="Закрыть">
<span data-icon="x"></span>
</button>
</div>
</div>
@ -361,7 +350,7 @@
<div class="plots-container">
<div class="plots-content">
<div class="plots-grid" id="plotsGrid">
<!-- Individual plots will be populated here -->
<!-- Здесь появятся отдельные графики -->
</div>
</div>
</div>
@ -373,7 +362,6 @@
<div class="notifications" id="notifications"></div>
<!-- Scripts -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<script type="module" src="/static/js/main.js"></script>
</body>
</html>