translated to russian and removed internet deps
This commit is contained in:
@ -1 +0,0 @@
|
|||||||
s21_start100_stop8800_points1000_bw1khz/bambambum
|
|
||||||
@ -200,13 +200,24 @@ class VNADataAcquisition:
|
|||||||
|
|
||||||
self._reset_sweep_state()
|
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
|
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)
|
dir_b = f.read(1)
|
||||||
if not dir_b:
|
if not dir_b:
|
||||||
# EOF reached; wait for more data to arrive on disk
|
# 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)
|
time.sleep(0.1)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"open_air": false,
|
"open_air": true,
|
||||||
"axis": "abs",
|
"axis": "abs",
|
||||||
"data_limitation": "ph_only_2",
|
"data_limitation": "ph_only_1",
|
||||||
"cut": 0.417,
|
"cut": 1.291,
|
||||||
"max": 0.6,
|
"max": 1.0,
|
||||||
"gain": 0.0,
|
"gain": 0.9,
|
||||||
"start_freq": 100.0,
|
"start_freq": 100.0,
|
||||||
"stop_freq": 8800.0,
|
"stop_freq": 8800.0,
|
||||||
"clear_history": false,
|
"clear_history": false,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"y_min": -80,
|
"y_min": -80,
|
||||||
"y_max": 40,
|
"y_max": 40,
|
||||||
"show_phase": false
|
"show_phase": true
|
||||||
}
|
}
|
||||||
@ -1 +1 @@
|
|||||||
s21_start100_stop8800_points1000_bw1khz/testet
|
s21_start100_stop8800_points1000_bw1khz/ффывфы
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "ффывфы",
|
||||||
|
"timestamp": "2025-09-30T16:45:34.508464",
|
||||||
|
"preset_filename": "s21_start100_stop8800_points1000_bw1khz.bin",
|
||||||
|
"description": "",
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,54 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<rect width="32" height="32" rx="6" fill="#1e293b"/>
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
<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"/>
|
<svg
|
||||||
<circle cx="16" cy="20" r="1.5" fill="#22c55e"/>
|
version="1.1"
|
||||||
<circle cx="20" cy="8" r="1.5" fill="#22c55e"/>
|
id="svg1"
|
||||||
<circle cx="24" cy="16" r="1.5" fill="#22c55e"/>
|
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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 2.3 KiB |
@ -1,10 +1,66 @@
|
|||||||
/* Acquisition Controls */
|
/* Acquisition Controls */
|
||||||
.acquisition-controls {
|
.acquisition-controls {
|
||||||
display: flex;
|
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);
|
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 {
|
.acquisition-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
|
/* Icons */
|
||||||
|
.icon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -71,17 +79,17 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn i {
|
.btn .icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--sm i {
|
.btn--sm .icon {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--lg i {
|
.btn--lg .icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
@ -267,7 +275,7 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card__action i {
|
.chart-card__action .icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
@ -517,7 +525,7 @@
|
|||||||
background-color: var(--bg-surface-hover);
|
background-color: var(--bg-surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__close i {
|
.notification__close .icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,7 @@ body {
|
|||||||
background-color: var(--color-primary-900);
|
background-color: var(--color-primary-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn i {
|
.nav-btn .icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -294,7 +294,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calibration-standard-btn i {
|
.calibration-standard-btn .icon {
|
||||||
margin-right: var(--space-2);
|
margin-right: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,7 +571,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reference-description:empty::before {
|
.reference-description:empty::before {
|
||||||
content: "No description provided";
|
content: "Описание не указано";
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { NotificationManager } from './modules/notifications.js';
|
|||||||
import { StorageManager } from './modules/storage.js';
|
import { StorageManager } from './modules/storage.js';
|
||||||
import { SettingsManager } from './modules/settings.js';
|
import { SettingsManager } from './modules/settings.js';
|
||||||
import { AcquisitionManager } from './modules/acquisition.js';
|
import { AcquisitionManager } from './modules/acquisition.js';
|
||||||
|
import { renderIcons } from './modules/icons.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Application Class
|
* Main Application Class
|
||||||
@ -65,10 +66,8 @@ class VNADashboard {
|
|||||||
try {
|
try {
|
||||||
console.log('Initializing VNA Dashboard...');
|
console.log('Initializing VNA Dashboard...');
|
||||||
|
|
||||||
// Initialize Lucide icons
|
// Render SVG icons
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons();
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize modules in correct order
|
// Initialize modules in correct order
|
||||||
await this.initializeModules();
|
await this.initializeModules();
|
||||||
@ -85,16 +84,16 @@ class VNADashboard {
|
|||||||
// Show welcome notification
|
// Show welcome notification
|
||||||
this.notifications.show({
|
this.notifications.show({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'Dashboard Ready',
|
title: 'Панель готова',
|
||||||
message: 'Connected to VNA System. Waiting for sweep data...'
|
message: 'Соединение с системой ВНА установлено. Ожидание данных свипа...'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize VNA Dashboard:', error);
|
console.error('Failed to initialize VNA Dashboard:', error);
|
||||||
this.notifications.show({
|
this.notifications.show({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Initialization Failed',
|
title: 'Сбой инициализации',
|
||||||
message: error.message || 'Failed to initialize dashboard'
|
message: error.message || 'Не удалось инициализировать панель'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,60 +56,63 @@ export class AcquisitionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleStartClick() {
|
async handleStartClick() {
|
||||||
|
let originalState;
|
||||||
try {
|
try {
|
||||||
const originalState = setButtonLoading(this.elements.startBtn, true);
|
originalState = setButtonLoading(this.elements.startBtn, true);
|
||||||
|
|
||||||
const result = await apiPost(API.ACQUISITION.START);
|
const result = await apiPost(API.ACQUISITION.START);
|
||||||
|
|
||||||
if (result.success) {
|
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();
|
await this.updateStatus();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error starting acquisition:', 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 {
|
} finally {
|
||||||
setButtonLoading(this.elements.startBtn, false, originalState);
|
setButtonLoading(this.elements.startBtn, false, originalState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleStopClick() {
|
async handleStopClick() {
|
||||||
|
let originalState;
|
||||||
try {
|
try {
|
||||||
const originalState = setButtonLoading(this.elements.stopBtn, true);
|
originalState = setButtonLoading(this.elements.stopBtn, true);
|
||||||
|
|
||||||
const result = await apiPost(API.ACQUISITION.STOP);
|
const result = await apiPost(API.ACQUISITION.STOP);
|
||||||
|
|
||||||
if (result.success) {
|
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();
|
await this.updateStatus();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error stopping acquisition:', 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 {
|
} finally {
|
||||||
setButtonLoading(this.elements.stopBtn, false, originalState);
|
setButtonLoading(this.elements.stopBtn, false, originalState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSingleSweepClick() {
|
async handleSingleSweepClick() {
|
||||||
|
let originalState;
|
||||||
try {
|
try {
|
||||||
const originalState = setButtonLoading(this.elements.singleSweepBtn, true);
|
originalState = setButtonLoading(this.elements.singleSweepBtn, true);
|
||||||
|
|
||||||
const result = await apiPost(API.ACQUISITION.SINGLE);
|
const result = await apiPost(API.ACQUISITION.SINGLE);
|
||||||
|
|
||||||
if (result.success) {
|
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();
|
await this.updateStatus();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error triggering single sweep:', 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 {
|
} finally {
|
||||||
setButtonLoading(this.elements.singleSweepBtn, false, originalState);
|
setButtonLoading(this.elements.singleSweepBtn, false, originalState);
|
||||||
}
|
}
|
||||||
@ -134,15 +137,15 @@ export class AcquisitionManager {
|
|||||||
|
|
||||||
updateUI(status) {
|
updateUI(status) {
|
||||||
// Update status text and indicator
|
// Update status text and indicator
|
||||||
let statusText = 'Idle';
|
let statusText = 'Ожидание';
|
||||||
let statusClass = 'status-indicator__dot--idle';
|
let statusClass = 'status-indicator__dot--idle';
|
||||||
|
|
||||||
if (status.running) {
|
if (status.running) {
|
||||||
if (status.paused) {
|
if (status.paused) {
|
||||||
statusText = 'Stopped';
|
statusText = 'Остановлено';
|
||||||
statusClass = 'status-indicator__dot--paused';
|
statusClass = 'status-indicator__dot--paused';
|
||||||
} else {
|
} else {
|
||||||
statusText = 'Running';
|
statusText = 'Выполняется';
|
||||||
statusClass = 'status-indicator__dot--running';
|
statusClass = 'status-indicator__dot--running';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +160,7 @@ export class AcquisitionManager {
|
|||||||
|
|
||||||
// Update mode text
|
// Update mode text
|
||||||
if (this.elements.modeText) {
|
if (this.elements.modeText) {
|
||||||
this.elements.modeText.textContent = status.continuous_mode ? 'Continuous' : 'Single';
|
this.elements.modeText.textContent = status.continuous_mode ? 'Непрерывный' : 'Одиночный';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update button states
|
// Update button states
|
||||||
@ -170,7 +173,7 @@ export class AcquisitionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.elements.singleSweepBtn) {
|
if (this.elements.singleSweepBtn) {
|
||||||
this.elements.singleSweepBtn.disabled = !status.running;
|
this.elements.singleSweepBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update sweep count in header if available
|
// Update sweep count in header if available
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
|
import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
|
||||||
|
import { renderIcons } from './icons.js';
|
||||||
import { ChartSettingsManager } from './charts/chart-settings.js';
|
import { ChartSettingsManager } from './charts/chart-settings.js';
|
||||||
import {
|
import {
|
||||||
defaultPlotlyLayout,
|
defaultPlotlyLayout,
|
||||||
@ -76,7 +77,9 @@ export class ChartManager {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error adding chart result:', e);
|
console.error('Error adding chart result:', e);
|
||||||
this.notifications?.show?.({
|
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 = `
|
card.innerHTML = `
|
||||||
<div class="chart-card__header">
|
<div class="chart-card__header">
|
||||||
<div class="chart-card__title">
|
<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)}
|
${formatProcessorName(processorId)}
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card__actions">
|
<div class="chart-card__actions">
|
||||||
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
|
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
|
||||||
<i data-lucide="expand"></i>
|
<span data-icon="expand"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="chart-card__action" data-action="download" title="Download">
|
<button class="chart-card__action" data-action="download" title="Download">
|
||||||
<i data-lucide="download"></i>
|
<span data-icon="download"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="chart-card__action" data-action="hide" title="Hide">
|
<button class="chart-card__action" data-action="hide" title="Hide">
|
||||||
<i data-lucide="eye-off"></i>
|
<span data-icon="eye-off"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,7 +202,7 @@ export class ChartManager {
|
|||||||
this.setupChartCardEvents(card, processorId);
|
this.setupChartCardEvents(card, processorId);
|
||||||
this.updateChartSettings(processorId);
|
this.updateChartSettings(processorId);
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
renderIcons(card);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,15 +300,15 @@ export class ChartManager {
|
|||||||
|
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Download Complete',
|
title: 'Скачивание завершено',
|
||||||
message: `Downloaded ${formatProcessorName(id)} plot and data`
|
message: `Скачаны график и данные ${formatProcessorName(id)}`
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Chart download failed:', e);
|
console.error('Chart download failed:', e);
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Download Failed',
|
title: 'Ошибка скачивания',
|
||||||
message: 'Failed to download chart data'
|
message: 'Не удалось скачать данные графика'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createParameterControl } from '../utils.js';
|
import { createParameterControl } from '../utils.js';
|
||||||
|
import { renderIcons } from '../icons.js';
|
||||||
|
|
||||||
export class ChartSettingsManager {
|
export class ChartSettingsManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -42,7 +43,7 @@ export class ChartSettingsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsContainer.innerHTML = '<div class="settings-empty">No settings available</div>';
|
settingsContainer.innerHTML = '<div class="settings-empty">Настройки недоступны</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,9 +63,7 @@ export class ChartSettingsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons(settingsContainer);
|
||||||
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEvents(settingsContainer, processorId) {
|
setupEvents(settingsContainer, processorId) {
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export const API = {
|
|||||||
SETTINGS: {
|
SETTINGS: {
|
||||||
PRESETS: `${API_BASE}/settings/presets`,
|
PRESETS: `${API_BASE}/settings/presets`,
|
||||||
PRESET_SET: `${API_BASE}/settings/preset/set`,
|
PRESET_SET: `${API_BASE}/settings/preset/set`,
|
||||||
|
PRESET_CURRENT: `${API_BASE}/settings/preset/current`,
|
||||||
STATUS: `${API_BASE}/settings/status`,
|
STATUS: `${API_BASE}/settings/status`,
|
||||||
|
|
||||||
CALIBRATIONS: `${API_BASE}/settings/calibrations`,
|
CALIBRATIONS: `${API_BASE}/settings/calibrations`,
|
||||||
|
|||||||
257
vna_system/web_ui/static/js/modules/icons.js
Normal file
257
vna_system/web_ui/static/js/modules/icons.js
Normal 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);
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { escapeHtml } from './utils.js';
|
import { escapeHtml } from './utils.js';
|
||||||
|
import { renderIcons } from './icons.js';
|
||||||
|
|
||||||
export class NotificationManager {
|
export class NotificationManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -166,10 +167,7 @@ export class NotificationManager {
|
|||||||
}, notification.timeout);
|
}, notification.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Lucide icons
|
renderIcons(notification.element);
|
||||||
if (typeof lucide !== 'undefined') {
|
|
||||||
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Showing ${notification.type} notification:`, notification.title);
|
console.log(` Showing ${notification.type} notification:`, notification.title);
|
||||||
}
|
}
|
||||||
@ -182,7 +180,7 @@ export class NotificationManager {
|
|||||||
element.className = `notification ${notification.config.class}`;
|
element.className = `notification ${notification.config.class}`;
|
||||||
element.dataset.id = notification.id;
|
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 ?
|
const titleHtml = notification.title ?
|
||||||
`<div class="notification__title">${escapeHtml(notification.title)}</div>` : '';
|
`<div class="notification__title">${escapeHtml(notification.title)}</div>` : '';
|
||||||
@ -194,8 +192,8 @@ export class NotificationManager {
|
|||||||
this.createActionsHtml(notification.actions) : '';
|
this.createActionsHtml(notification.actions) : '';
|
||||||
|
|
||||||
const closeHtml = `
|
const closeHtml = `
|
||||||
<button class="notification__close" data-action="close" title="Close">
|
<button class="notification__close" data-action="close" title="Закрыть">
|
||||||
<i data-lucide="x"></i>
|
<span data-icon="x"></span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -225,7 +223,7 @@ export class NotificationManager {
|
|||||||
const buttonClass = action.primary ? 'btn btn--primary btn--sm' : 'btn btn--ghost btn--sm';
|
const buttonClass = action.primary ? 'btn btn--primary btn--sm' : 'btn btn--ghost btn--sm';
|
||||||
return `
|
return `
|
||||||
<button class="${buttonClass}" data-action="custom" data-action-id="${action.id}">
|
<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)}
|
${escapeHtml(action.label)}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { PresetManager } from './settings/preset-manager.js';
|
|||||||
import { CalibrationManager } from './settings/calibration-manager.js';
|
import { CalibrationManager } from './settings/calibration-manager.js';
|
||||||
import { ReferenceManager } from './settings/reference-manager.js';
|
import { ReferenceManager } from './settings/reference-manager.js';
|
||||||
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
|
import { Debouncer, ButtonState, downloadJSON } from './utils.js';
|
||||||
|
import { renderIcons } from './icons.js';
|
||||||
import {
|
import {
|
||||||
createPlotlyPlot,
|
createPlotlyPlot,
|
||||||
togglePlotlyFullscreen,
|
togglePlotlyFullscreen,
|
||||||
@ -53,7 +54,7 @@ export class SettingsManager {
|
|||||||
console.log('Settings Manager initialized');
|
console.log('Settings Manager initialized');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Settings Manager init failed:', 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
|
// Setup callbacks
|
||||||
this.presetManager.onPresetChanged = async () => {
|
this.presetManager.onPresetChanged = async () => {
|
||||||
await this.loadStatus();
|
const status = await this.loadStatus();
|
||||||
this.calibrationManager.reset();
|
this.calibrationManager.reset();
|
||||||
await this.calibrationManager.loadWorkingCalibration();
|
await this.calibrationManager.loadWorkingCalibration();
|
||||||
|
await this.ensureCurrentPreset(status);
|
||||||
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
||||||
};
|
};
|
||||||
|
|
||||||
this.calibrationManager.onCalibrationSaved = async () => {
|
this.calibrationManager.onCalibrationSaved = async () => {
|
||||||
await this.loadStatus();
|
const status = await this.loadStatus();
|
||||||
|
await this.ensureCurrentPreset(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.calibrationManager.onCalibrationSet = async () => {
|
this.calibrationManager.onCalibrationSet = async () => {
|
||||||
await this.loadStatus();
|
const status = await this.loadStatus();
|
||||||
|
await this.ensureCurrentPreset(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.referenceManager.onReferenceUpdated = (reference) => {
|
this.referenceManager.onReferenceUpdated = (reference) => {
|
||||||
@ -141,30 +145,91 @@ export class SettingsManager {
|
|||||||
|
|
||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
await this.presetManager.loadPresets();
|
await this.presetManager.loadPresets();
|
||||||
await this.loadStatus();
|
const status = await this.loadStatus();
|
||||||
await this.calibrationManager.loadWorkingCalibration();
|
await this.calibrationManager.loadWorkingCalibration();
|
||||||
|
await this.ensureCurrentPreset(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadStatus() {
|
async loadStatus() {
|
||||||
try {
|
try {
|
||||||
const status = await apiGet(API.SETTINGS.STATUS);
|
const status = await apiGet(API.SETTINGS.STATUS);
|
||||||
this.updateStatusDisplay(status);
|
await this.updateStatusDisplay(status);
|
||||||
|
return status;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Status load failed:', 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.presetManager.updateStatus(status);
|
||||||
this.calibrationManager.updateStatus(status);
|
this.calibrationManager.updateStatus(status);
|
||||||
|
|
||||||
const preset = this.presetManager.getCurrentPreset();
|
const preset = this.presetManager.getCurrentPreset();
|
||||||
this.calibrationManager.setCurrentPreset(preset);
|
this.calibrationManager.setCurrentPreset(preset);
|
||||||
this.referenceManager.setCurrentPreset(preset);
|
await this.referenceManager.setCurrentPreset(preset);
|
||||||
|
|
||||||
this.elements.presetCount.textContent = status.available_presets || 0;
|
if (this.elements.presetCount) {
|
||||||
this.elements.calibrationCount.textContent = status.available_calibrations || 0;
|
this.elements.presetCount.textContent = status?.available_presets ?? '-';
|
||||||
this.elements.systemStatus.textContent = 'Ready';
|
}
|
||||||
|
if (this.elements.calibrationCount) {
|
||||||
|
this.elements.calibrationCount.textContent = status?.available_calibrations ?? '-';
|
||||||
|
}
|
||||||
|
if (this.elements.systemStatus) {
|
||||||
|
this.elements.systemStatus.textContent = 'Готово';
|
||||||
|
}
|
||||||
|
|
||||||
this.updateHeaderSummary(status);
|
this.updateHeaderSummary(status);
|
||||||
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
this.updateReferenceSummary(this.referenceManager.getCurrentReference());
|
||||||
@ -177,7 +242,7 @@ export class SettingsManager {
|
|||||||
if (!name || !preset) return;
|
if (!name || !preset) return;
|
||||||
|
|
||||||
try {
|
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), {
|
const url = buildUrl(API.SETTINGS.CALIBRATION_STANDARDS_PLOTS(name), {
|
||||||
preset_filename: preset.filename
|
preset_filename: preset.filename
|
||||||
@ -187,9 +252,9 @@ export class SettingsManager {
|
|||||||
this.showPlotsModal(plotsData);
|
this.showPlotsModal(plotsData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Load plots failed:', e);
|
console.error('Load plots failed:', e);
|
||||||
this.notify(ERROR, 'Plots Error', 'Failed to load calibration plots');
|
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики калибровки');
|
||||||
} finally {
|
} 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);
|
}, TIMING.DEBOUNCE_CALIBRATION);
|
||||||
}
|
}
|
||||||
@ -200,19 +265,19 @@ export class SettingsManager {
|
|||||||
if (!working || !working.active) return;
|
if (!working || !working.active) return;
|
||||||
|
|
||||||
try {
|
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);
|
const plotsData = await apiGet(API.SETTINGS.WORKING_CALIBRATION_PLOTS);
|
||||||
this.showPlotsModal(plotsData);
|
this.showPlotsModal(plotsData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 404) {
|
if (e.status === 404) {
|
||||||
this.notify(WARNING, 'No Data', 'No working calibration or standards available to plot');
|
this.notify(WARNING, 'Нет данных', 'Нет активной калибровки или стандартов для отображения');
|
||||||
} else {
|
} else {
|
||||||
console.error('Load current plots failed:', e);
|
console.error('Load current plots failed:', e);
|
||||||
this.notify(ERROR, 'Plots Error', 'Failed to load current calibration plots');
|
this.notify(ERROR, 'Ошибка графиков', 'Не удалось загрузить графики текущей калибровки');
|
||||||
}
|
}
|
||||||
} finally {
|
} 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);
|
}, TIMING.DEBOUNCE_CALIBRATION);
|
||||||
}
|
}
|
||||||
@ -228,10 +293,10 @@ export class SettingsManager {
|
|||||||
const title = modal.querySelector('.modal__title');
|
const title = modal.querySelector('.modal__title');
|
||||||
if (title) {
|
if (title) {
|
||||||
title.innerHTML = `
|
title.innerHTML = `
|
||||||
<i data-lucide="bar-chart-3"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
|
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
|
||||||
`;
|
`;
|
||||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
renderIcons(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupModalCloseHandlers(modal);
|
this.setupModalCloseHandlers(modal);
|
||||||
@ -247,7 +312,7 @@ export class SettingsManager {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
if (!individualPlots || !Object.keys(individualPlots).length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,7 +323,7 @@ export class SettingsManager {
|
|||||||
err.innerHTML = `
|
err.innerHTML = `
|
||||||
<div class="chart-card__header">
|
<div class="chart-card__header">
|
||||||
<div class="chart-card__title">
|
<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
|
${name.toUpperCase()} Standard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -266,6 +331,7 @@ export class SettingsManager {
|
|||||||
<div class="plot-error">Error: ${plot.error}</div>
|
<div class="plot-error">Error: ${plot.error}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
renderIcons(err);
|
||||||
container.appendChild(err);
|
container.appendChild(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -274,9 +340,7 @@ export class SettingsManager {
|
|||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons(container);
|
||||||
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createCalibrationChartCard(standardName, plotConfig, preset) {
|
createCalibrationChartCard(standardName, plotConfig, preset) {
|
||||||
@ -284,20 +348,20 @@ export class SettingsManager {
|
|||||||
card.className = 'chart-card';
|
card.className = 'chart-card';
|
||||||
card.dataset.standard = standardName;
|
card.dataset.standard = standardName;
|
||||||
|
|
||||||
const title = `${standardName.toUpperCase()} Standard`;
|
const title = `Стандарт ${standardName.toUpperCase()}`;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="chart-card__header">
|
<div class="chart-card__header">
|
||||||
<div class="chart-card__title">
|
<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}
|
${title}
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card__actions">
|
<div class="chart-card__actions">
|
||||||
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
|
<button class="chart-card__action" data-action="fullscreen" title="На весь экран">
|
||||||
<i data-lucide="expand"></i>
|
<span data-icon="expand"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="chart-card__action" data-action="download" title="Download">
|
<button class="chart-card__action" data-action="download" title="Скачать">
|
||||||
<i data-lucide="download"></i>
|
<span data-icon="download"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -305,8 +369,8 @@ export class SettingsManager {
|
|||||||
<div class="chart-card__plot" id="calibration-plot-${standardName}"></div>
|
<div class="chart-card__plot" id="calibration-plot-${standardName}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card__meta">
|
<div class="chart-card__meta">
|
||||||
<div class="chart-card__timestamp">Standard: ${standardName.toUpperCase()}</div>
|
<div class="chart-card__timestamp">Стандарт: ${standardName.toUpperCase()}</div>
|
||||||
<div class="chart-card__sweep">Preset: ${preset?.filename || 'Unknown'}</div>
|
<div class="chart-card__sweep">Пресет: ${preset?.filename || 'Неизвестно'}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -322,13 +386,14 @@ export class SettingsManager {
|
|||||||
|
|
||||||
const plotEl = card.querySelector('.chart-card__plot');
|
const plotEl = card.querySelector('.chart-card__plot');
|
||||||
this.renderPlotly(plotEl, plotConfig, title);
|
this.renderPlotly(plotEl, plotConfig, title);
|
||||||
|
renderIcons(card);
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPlotly(container, plotConfig, title) {
|
renderPlotly(container, plotConfig, title) {
|
||||||
if (!container || !plotConfig || plotConfig.error) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,10 +480,10 @@ export class SettingsManager {
|
|||||||
const data = this.prepareCalibrationDownloadData(standardName);
|
const data = this.prepareCalibrationDownloadData(standardName);
|
||||||
downloadJSON(data, `${base}_data.json`);
|
downloadJSON(data, `${base}_data.json`);
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Download Complete', `Downloaded ${standardName.toUpperCase()} standard plot and data`);
|
this.notify(SUCCESS, 'Скачивание завершено', `Скачаны график и данные стандарта ${standardName.toUpperCase()}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Download standard failed:', 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 base = `${calibrationName}_complete_${ts}`;
|
||||||
|
|
||||||
const btn = this.elements.downloadAllBtn;
|
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 = {
|
const complete = {
|
||||||
export_info: {
|
export_info: {
|
||||||
@ -444,13 +509,13 @@ export class SettingsManager {
|
|||||||
|
|
||||||
downloadJSON(complete, `${base}.json`);
|
downloadJSON(complete, `${base}.json`);
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Complete Download', `Downloaded complete calibration data for ${calibrationName}`);
|
this.notify(SUCCESS, 'Полное скачивание завершено', `Полный набор данных калибровки сохранён для ${calibrationName}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Download all failed:', e);
|
console.error('Download all failed:', e);
|
||||||
this.notify(ERROR, 'Download Failed', 'Failed to download complete calibration data');
|
this.notify(ERROR, 'Ошибка скачивания', 'Не удалось скачать полный набор данных калибровки');
|
||||||
} finally {
|
} finally {
|
||||||
const btn = this.elements.downloadAllBtn;
|
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;
|
if (!Number.isFinite(numeric)) return null;
|
||||||
const abs = Math.abs(numeric);
|
const abs = Math.abs(numeric);
|
||||||
const units = [
|
const units = [
|
||||||
{ divider: 1e9, suffix: 'GHz' },
|
{ divider: 1e9, suffix: 'ГГц' },
|
||||||
{ divider: 1e6, suffix: 'MHz' },
|
{ divider: 1e6, suffix: 'МГц' },
|
||||||
{ divider: 1e3, suffix: 'kHz' }
|
{ divider: 1e3, suffix: 'кГц' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const unit of units) {
|
for (const unit of units) {
|
||||||
@ -504,11 +569,11 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${numeric.toFixed(0)} Hz`;
|
return `${numeric.toFixed(0)} Гц`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatPresetSummary(preset) {
|
formatPresetSummary(preset) {
|
||||||
if (!preset) return 'Not selected';
|
if (!preset) return 'Не выбран';
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (preset.mode) {
|
if (preset.mode) {
|
||||||
@ -524,31 +589,31 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (preset.points) {
|
if (preset.points) {
|
||||||
parts.push(`${preset.points} pts`);
|
parts.push(`${preset.points} точек`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preset.bandwidth) {
|
if (preset.bandwidth) {
|
||||||
const bw = this.formatFrequency(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) {
|
formatCalibrationSummary(status) {
|
||||||
const active = status?.current_calibration;
|
const active = status?.current_calibration;
|
||||||
if (active?.calibration_name) {
|
if (active?.calibration_name) {
|
||||||
return `Active • ${active.calibration_name}`;
|
return `Активна • ${active.calibration_name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const working = status?.working_calibration;
|
const working = status?.working_calibration;
|
||||||
if (working?.progress) {
|
if (working?.progress) {
|
||||||
const missingCount = working.missing_standards?.length || 0;
|
const missingCount = working.missing_standards?.length || 0;
|
||||||
const missingText = missingCount ? ` • ${missingCount} missing` : '';
|
const missingText = missingCount ? ` • не хватает ${missingCount} стандартов` : '';
|
||||||
return `In progress • ${working.progress}${missingText}`;
|
return `В процессе • ${working.progress}${missingText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Not set';
|
return 'Не задано';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderSummary(status) {
|
updateHeaderSummary(status) {
|
||||||
@ -567,11 +632,11 @@ export class SettingsManager {
|
|||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
if (!reference) {
|
if (!reference) {
|
||||||
target.textContent = 'Not captured';
|
target.textContent = 'Не снят';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = reference.name || 'Reference';
|
const name = reference.name || 'Эталон';
|
||||||
let timestampText = '';
|
let timestampText = '';
|
||||||
if (reference.timestamp) {
|
if (reference.timestamp) {
|
||||||
const timestamp = new Date(reference.timestamp);
|
const timestamp = new Date(reference.timestamp);
|
||||||
@ -580,7 +645,7 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const summaryParts = ['Active', name];
|
const summaryParts = ['Активен', name];
|
||||||
if (timestampText) {
|
if (timestampText) {
|
||||||
summaryParts.push(timestampText);
|
summaryParts.push(timestampText);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
|
import { Debouncer, RequestGuard, ButtonState } from '../utils.js';
|
||||||
|
import { renderIcons } from '../icons.js';
|
||||||
import { apiGet, apiPost, buildUrl } from '../api-client.js';
|
import { apiGet, apiPost, 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';
|
||||||
|
|
||||||
@ -86,14 +87,14 @@ export class CalibrationManager {
|
|||||||
dd.innerHTML = '';
|
dd.innerHTML = '';
|
||||||
|
|
||||||
if (!calibrations.length) {
|
if (!calibrations.length) {
|
||||||
dd.innerHTML = '<option value="">No calibrations available</option>';
|
dd.innerHTML = '<option value="">Калибровки отсутствуют</option>';
|
||||||
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;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dd.innerHTML = '<option value="">Select calibration...</option>';
|
dd.innerHTML = '<option value="">Выберите калибровку...</option>';
|
||||||
calibrations.forEach(c => {
|
calibrations.forEach(c => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = c.name;
|
opt.value = c.name;
|
||||||
@ -158,19 +159,19 @@ export class CalibrationManager {
|
|||||||
|
|
||||||
if (capturing) {
|
if (capturing) {
|
||||||
btn.classList.add('btn--warning');
|
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.disabled = true;
|
||||||
btn.title = 'Standard is currently being captured';
|
btn.title = 'Стандарт сейчас снимается';
|
||||||
} else if (isCompleted) {
|
} else if (isCompleted) {
|
||||||
btn.classList.add('btn--success');
|
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.disabled = false;
|
||||||
btn.title = 'Click to recapture this standard';
|
btn.title = 'Нажмите, чтобы переснять стандарт';
|
||||||
} else if (isMissing) {
|
} else if (isMissing) {
|
||||||
btn.classList.add('btn--primary');
|
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.disabled = false;
|
||||||
btn.title = 'Click to capture this standard';
|
btn.title = 'Нажмите, чтобы снять стандарт';
|
||||||
} else {
|
} else {
|
||||||
btn.classList.add('btn--secondary');
|
btn.classList.add('btn--secondary');
|
||||||
btn.innerHTML = `${std.toUpperCase()}`;
|
btn.innerHTML = `${std.toUpperCase()}`;
|
||||||
@ -181,9 +182,7 @@ export class CalibrationManager {
|
|||||||
container.appendChild(btn);
|
container.appendChild(btn);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons(container);
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStandardsForMode() {
|
getStandardsForMode() {
|
||||||
@ -204,20 +203,20 @@ export class CalibrationManager {
|
|||||||
this.debouncer.debounce('start-calibration', () =>
|
this.debouncer.debounce('start-calibration', () =>
|
||||||
this.reqGuard.runExclusive('start-calibration', async () => {
|
this.reqGuard.runExclusive('start-calibration', async () => {
|
||||||
try {
|
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, {
|
const result = await apiPost(API.SETTINGS.CALIBRATION_START, {
|
||||||
preset_filename: this.currentPreset.filename
|
preset_filename: this.currentPreset.filename
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notify(INFO, 'Calibration Started', `Started calibration for ${result.preset}`);
|
this.notify(INFO, 'Калибровка запущена', `Запущена калибровка для ${result.preset}`);
|
||||||
|
|
||||||
await this.loadWorkingCalibration();
|
await this.loadWorkingCalibration();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Start calibration failed:', e);
|
console.error('Start calibration failed:', e);
|
||||||
this.notify(ERROR, 'Calibration Error', 'Failed to start calibration');
|
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось запустить калибровку');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_CALIBRATION
|
||||||
);
|
);
|
||||||
@ -233,19 +232,19 @@ export class CalibrationManager {
|
|||||||
this.disabledStandards.add(standard);
|
this.disabledStandards.add(standard);
|
||||||
|
|
||||||
const btn = document.querySelector(`[data-standard="${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 });
|
const result = await apiPost(API.SETTINGS.CALIBRATION_ADD_STANDARD, { standard });
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Standard Captured', result.message);
|
this.notify(SUCCESS, 'Стандарт снят', result.message);
|
||||||
|
|
||||||
this.resetCaptureState();
|
this.resetCaptureState();
|
||||||
await this.loadWorkingCalibration();
|
await this.loadWorkingCalibration();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Capture standard failed:', e);
|
console.error('Capture standard failed:', e);
|
||||||
this.notify(ERROR, 'Calibration Error', 'Failed to capture calibration standard');
|
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось снять стандарт калибровки');
|
||||||
this.resetCaptureState(standard);
|
this.resetCaptureState(standard);
|
||||||
}
|
}
|
||||||
}), TIMING.DEBOUNCE_CALIBRATION
|
}), TIMING.DEBOUNCE_CALIBRATION
|
||||||
@ -259,11 +258,11 @@ export class CalibrationManager {
|
|||||||
this.debouncer.debounce('save-calibration', () =>
|
this.debouncer.debounce('save-calibration', () =>
|
||||||
this.reqGuard.runExclusive('save-calibration', async () => {
|
this.reqGuard.runExclusive('save-calibration', async () => {
|
||||||
try {
|
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 });
|
const result = await apiPost(API.SETTINGS.CALIBRATION_SAVE, { name });
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Calibration Saved', result.message);
|
this.notify(SUCCESS, 'Калибровка сохранена', result.message);
|
||||||
|
|
||||||
this.hideSteps();
|
this.hideSteps();
|
||||||
this.elements.calibrationNameInput.value = '';
|
this.elements.calibrationNameInput.value = '';
|
||||||
@ -276,9 +275,9 @@ export class CalibrationManager {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Save calibration failed:', e);
|
console.error('Save calibration failed:', e);
|
||||||
this.notify(ERROR, 'Calibration Error', 'Failed to save calibration');
|
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось сохранить калибровку');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_CALIBRATION
|
||||||
);
|
);
|
||||||
@ -291,23 +290,23 @@ export class CalibrationManager {
|
|||||||
if (!name || !this.currentPreset) return;
|
if (!name || !this.currentPreset) return;
|
||||||
|
|
||||||
try {
|
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, {
|
const result = await apiPost(API.SETTINGS.CALIBRATION_SET, {
|
||||||
name,
|
name,
|
||||||
preset_filename: this.currentPreset.filename
|
preset_filename: this.currentPreset.filename
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Calibration Set', result.message);
|
this.notify(SUCCESS, 'Калибровка применена', result.message);
|
||||||
|
|
||||||
if (this.onCalibrationSet) {
|
if (this.onCalibrationSet) {
|
||||||
await this.onCalibrationSet();
|
await this.onCalibrationSet();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Set calibration failed:', e);
|
console.error('Set calibration failed:', e);
|
||||||
this.notify(ERROR, 'Calibration Error', 'Failed to set active calibration');
|
this.notify(ERROR, 'Ошибка калибровки', 'Не удалось применить калибровку');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_CALIBRATION
|
||||||
);
|
);
|
||||||
@ -319,7 +318,7 @@ export class CalibrationManager {
|
|||||||
this.elements.currentCalibration.textContent = status.current_calibration.calibration_name;
|
this.elements.currentCalibration.textContent = status.current_calibration.calibration_name;
|
||||||
} else {
|
} else {
|
||||||
this.currentCalibration = null;
|
this.currentCalibration = null;
|
||||||
this.elements.currentCalibration.textContent = 'None';
|
this.elements.currentCalibration.textContent = 'Нет';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class PresetManager {
|
|||||||
this.populateDropdown(presets);
|
this.populateDropdown(presets);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Presets load failed:', 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 = '';
|
dd.innerHTML = '';
|
||||||
|
|
||||||
if (!presets.length) {
|
if (!presets.length) {
|
||||||
dd.innerHTML = '<option value="">No presets available</option>';
|
dd.innerHTML = '<option value="">Пресеты отсутствуют</option>';
|
||||||
dd.disabled = true;
|
dd.disabled = true;
|
||||||
if (this.elements.setPresetBtn) {
|
if (this.elements.setPresetBtn) {
|
||||||
this.elements.setPresetBtn.disabled = true;
|
this.elements.setPresetBtn.disabled = true;
|
||||||
@ -55,7 +55,7 @@ export class PresetManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dd.innerHTML = '<option value="">Select preset...</option>';
|
dd.innerHTML = '<option value="">Выберите пресет...</option>';
|
||||||
presets.forEach(p => {
|
presets.forEach(p => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = p.filename;
|
opt.value = p.filename;
|
||||||
@ -89,11 +89,11 @@ export class PresetManager {
|
|||||||
this.debouncer.debounce('set-preset', () =>
|
this.debouncer.debounce('set-preset', () =>
|
||||||
this.reqGuard.runExclusive('set-preset', async () => {
|
this.reqGuard.runExclusive('set-preset', async () => {
|
||||||
try {
|
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 });
|
const result = await apiPost(API.SETTINGS.PRESET_SET, { filename });
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Preset Set', result.message);
|
this.notify(SUCCESS, 'Пресет применён', result.message);
|
||||||
this.currentPreset = { filename };
|
this.currentPreset = { filename };
|
||||||
|
|
||||||
if (this.onPresetChanged) {
|
if (this.onPresetChanged) {
|
||||||
@ -102,9 +102,9 @@ export class PresetManager {
|
|||||||
this.syncSelectedPreset();
|
this.syncSelectedPreset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Set preset failed:', e);
|
console.error('Set preset failed:', e);
|
||||||
this.notify(ERROR, 'Preset Error', 'Failed to set configuration preset');
|
this.notify(ERROR, 'Ошибка пресета', 'Не удалось применить пресет');
|
||||||
} finally {
|
} 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();
|
this.updateSetButtonState();
|
||||||
}
|
}
|
||||||
}), TIMING.DEBOUNCE_PRESET
|
}), TIMING.DEBOUNCE_PRESET
|
||||||
@ -117,7 +117,7 @@ export class PresetManager {
|
|||||||
this.elements.currentPreset.textContent = status.current_preset.filename;
|
this.elements.currentPreset.textContent = status.current_preset.filename;
|
||||||
} else {
|
} else {
|
||||||
this.currentPreset = null;
|
this.currentPreset = null;
|
||||||
this.elements.currentPreset.textContent = 'None';
|
this.elements.currentPreset.textContent = 'Нет';
|
||||||
}
|
}
|
||||||
this.syncSelectedPreset();
|
this.syncSelectedPreset();
|
||||||
}
|
}
|
||||||
@ -126,6 +126,14 @@ export class PresetManager {
|
|||||||
return this.currentPreset;
|
return this.currentPreset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCurrentPresetDirect(preset) {
|
||||||
|
this.currentPreset = preset;
|
||||||
|
if (this.elements.currentPreset) {
|
||||||
|
this.elements.currentPreset.textContent = preset ? preset.filename : 'Нет';
|
||||||
|
}
|
||||||
|
this.syncSelectedPreset();
|
||||||
|
}
|
||||||
|
|
||||||
syncSelectedPreset() {
|
syncSelectedPreset() {
|
||||||
const dd = this.elements.presetDropdown;
|
const dd = this.elements.presetDropdown;
|
||||||
if (!dd) return;
|
if (!dd) return;
|
||||||
|
|||||||
@ -44,10 +44,10 @@ export class ReferenceManager {
|
|||||||
this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference);
|
this.elements.deleteReferenceBtn?.removeEventListener('click', this.handleDeleteReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPreset(preset) {
|
async setCurrentPreset(preset) {
|
||||||
this.currentPreset = preset;
|
this.currentPreset = preset;
|
||||||
if (preset) {
|
if (preset) {
|
||||||
this.loadReferences();
|
await this.loadReferences();
|
||||||
} else {
|
} else {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
@ -89,14 +89,18 @@ export class ReferenceManager {
|
|||||||
|
|
||||||
this.elements.referenceDropdown.innerHTML = '';
|
this.elements.referenceDropdown.innerHTML = '';
|
||||||
|
|
||||||
|
const setBtn = this.elements.setReferenceBtn;
|
||||||
|
const deleteBtn = this.elements.deleteReferenceBtn;
|
||||||
|
const clearBtn = this.elements.clearReferenceBtn;
|
||||||
|
|
||||||
if (references.length === 0) {
|
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.referenceDropdown.disabled = true;
|
||||||
this.elements.setReferenceBtn.disabled = true;
|
if (setBtn) setBtn.disabled = true;
|
||||||
this.elements.clearReferenceBtn.disabled = true;
|
if (clearBtn) clearBtn.disabled = true;
|
||||||
this.elements.deleteReferenceBtn.disabled = true;
|
if (deleteBtn) deleteBtn.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
this.elements.referenceDropdown.innerHTML = '<option value="">Select a reference...</option>';
|
this.elements.referenceDropdown.innerHTML = '<option value="">Выберите эталон...</option>';
|
||||||
|
|
||||||
references.forEach(ref => {
|
references.forEach(ref => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
@ -110,18 +114,28 @@ export class ReferenceManager {
|
|||||||
if (this.currentReference) {
|
if (this.currentReference) {
|
||||||
this.elements.referenceDropdown.value = this.currentReference.name;
|
this.elements.referenceDropdown.value = this.currentReference.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setBtn) setBtn.disabled = false;
|
||||||
|
if (deleteBtn) deleteBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateButtons();
|
this.updateButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
const hasCurrent = this.currentReference !== null;
|
||||||
|
|
||||||
this.elements.setReferenceBtn.disabled = !hasSelection;
|
const setBtn = this.elements.setReferenceBtn;
|
||||||
this.elements.deleteReferenceBtn.disabled = !hasSelection;
|
const deleteBtn = this.elements.deleteReferenceBtn;
|
||||||
this.elements.clearReferenceBtn.disabled = !hasCurrent;
|
const clearBtn = this.elements.clearReferenceBtn;
|
||||||
|
|
||||||
|
if (setBtn) setBtn.disabled = !hasSelection;
|
||||||
|
if (deleteBtn) deleteBtn.disabled = !hasSelection;
|
||||||
|
if (clearBtn) clearBtn.disabled = !hasCurrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInfo(reference) {
|
updateInfo(reference) {
|
||||||
@ -161,7 +175,7 @@ export class ReferenceManager {
|
|||||||
async handleCreateReference() {
|
async handleCreateReference() {
|
||||||
const name = this.elements.referenceNameInput.value.trim();
|
const name = this.elements.referenceNameInput.value.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
this.notify(WARNING, 'Missing Name', 'Please enter a name for the reference');
|
this.notify(WARNING, 'Нет имени', 'Введите имя эталона');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,11 +184,11 @@ export class ReferenceManager {
|
|||||||
this.debouncer.debounce('create-reference', () =>
|
this.debouncer.debounce('create-reference', () =>
|
||||||
this.reqGuard.runExclusive('create-reference', async () => {
|
this.reqGuard.runExclusive('create-reference', async () => {
|
||||||
try {
|
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 });
|
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.referenceNameInput.value = '';
|
||||||
this.elements.referenceDescriptionInput.value = '';
|
this.elements.referenceDescriptionInput.value = '';
|
||||||
@ -182,9 +196,9 @@ export class ReferenceManager {
|
|||||||
await this.loadReferences();
|
await this.loadReferences();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create reference failed:', error);
|
console.error('Create reference failed:', error);
|
||||||
this.notify(ERROR, 'Reference Error', 'Failed to create reference');
|
this.notify(ERROR, 'Ошибка эталона', 'Не удалось создать эталон');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_REFERENCE
|
||||||
);
|
);
|
||||||
@ -201,18 +215,18 @@ export class ReferenceManager {
|
|||||||
this.debouncer.debounce('set-reference', () =>
|
this.debouncer.debounce('set-reference', () =>
|
||||||
this.reqGuard.runExclusive('set-reference', async () => {
|
this.reqGuard.runExclusive('set-reference', async () => {
|
||||||
try {
|
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 });
|
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();
|
await this.loadReferences();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Set reference failed:', error);
|
console.error('Set reference failed:', error);
|
||||||
this.notify(ERROR, 'Reference Error', 'Failed to set reference');
|
this.notify(ERROR, 'Ошибка эталона', 'Не удалось применить эталон');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_REFERENCE
|
||||||
);
|
);
|
||||||
@ -222,18 +236,24 @@ export class ReferenceManager {
|
|||||||
this.debouncer.debounce('clear-reference', () =>
|
this.debouncer.debounce('clear-reference', () =>
|
||||||
this.reqGuard.runExclusive('clear-reference', async () => {
|
this.reqGuard.runExclusive('clear-reference', async () => {
|
||||||
try {
|
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);
|
const result = await apiDelete(API.SETTINGS.REFERENCE_CURRENT);
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Reference Cleared', result.message);
|
this.notify(SUCCESS, 'Эталон сброшен', result.message);
|
||||||
|
|
||||||
await this.loadReferences();
|
await this.loadReferences();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Clear reference failed:', error);
|
console.error('Clear reference failed:', error);
|
||||||
this.notify(ERROR, 'Reference Error', 'Failed to clear reference');
|
this.notify(ERROR, 'Ошибка эталона', 'Не удалось сбросить эталон');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_REFERENCE
|
||||||
);
|
);
|
||||||
@ -250,18 +270,18 @@ export class ReferenceManager {
|
|||||||
this.debouncer.debounce('delete-reference', () =>
|
this.debouncer.debounce('delete-reference', () =>
|
||||||
this.reqGuard.runExclusive('delete-reference', async () => {
|
this.reqGuard.runExclusive('delete-reference', async () => {
|
||||||
try {
|
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));
|
const result = await apiDelete(API.SETTINGS.REFERENCE_ITEM(referenceName));
|
||||||
|
|
||||||
this.notify(SUCCESS, 'Reference Deleted', result.message);
|
this.notify(SUCCESS, 'Эталон удалён', result.message);
|
||||||
|
|
||||||
await this.loadReferences();
|
await this.loadReferences();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete reference failed:', error);
|
console.error('Delete reference failed:', error);
|
||||||
this.notify(ERROR, 'Reference Error', 'Failed to delete reference');
|
this.notify(ERROR, 'Ошибка эталона', 'Не удалось удалить эталон');
|
||||||
} finally {
|
} 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
|
}), TIMING.DEBOUNCE_REFERENCE
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatProcessorName, debounce } from './utils.js';
|
import { formatProcessorName, debounce } from './utils.js';
|
||||||
|
import { renderIcons } from './icons.js';
|
||||||
|
|
||||||
export class UIManager {
|
export class UIManager {
|
||||||
constructor(notifications, websocket, charts) {
|
constructor(notifications, websocket, charts) {
|
||||||
@ -157,16 +158,16 @@ export class UIManager {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 'connected':
|
case 'connected':
|
||||||
statusElement.classList.add('status-indicator--connected');
|
statusElement.classList.add('status-indicator--connected');
|
||||||
textElement.textContent = 'Connected';
|
textElement.textContent = 'Подключено';
|
||||||
break;
|
break;
|
||||||
case 'connecting':
|
case 'connecting':
|
||||||
statusElement.classList.add('status-indicator--connecting');
|
statusElement.classList.add('status-indicator--connecting');
|
||||||
textElement.textContent = 'Connecting...';
|
textElement.textContent = 'Подключение...';
|
||||||
break;
|
break;
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
default:
|
default:
|
||||||
statusElement.classList.add('status-indicator--disconnected');
|
statusElement.classList.add('status-indicator--disconnected');
|
||||||
textElement.textContent = 'Disconnected';
|
textElement.textContent = 'Отключено';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,9 +219,7 @@ export class UIManager {
|
|||||||
container.appendChild(toggle);
|
container.appendChild(toggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons(container);
|
||||||
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
/**
|
import { renderIcons } from './icons.js';
|
||||||
* Shared Utility Functions
|
|
||||||
* Common utilities used across the application
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format processor name: convert snake_case to Title Case
|
* Format processor name: convert snake_case to Title Case
|
||||||
@ -100,22 +97,20 @@ export class ButtonState {
|
|||||||
switch (state) {
|
switch (state) {
|
||||||
case 'loading':
|
case 'loading':
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
element.innerHTML = `<i data-lucide="${icon || 'loader'}"></i> ${text || 'Loading...'}`;
|
element.innerHTML = `<span data-icon="${icon || 'loader'}"></span> ${text || 'Загрузка...'}`;
|
||||||
break;
|
break;
|
||||||
case 'disabled':
|
case 'disabled':
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
element.innerHTML = icon ? `<i data-lucide="${icon}"></i> ${text}` : text;
|
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
|
||||||
break;
|
break;
|
||||||
case 'normal':
|
case 'normal':
|
||||||
default:
|
default:
|
||||||
element.disabled = !!disabled;
|
element.disabled = !!disabled;
|
||||||
element.innerHTML = icon ? `<i data-lucide="${icon}"></i> ${text}` : text;
|
element.innerHTML = icon ? `<span data-icon="${icon}"></span> ${text}` : text;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof lucide !== 'undefined') {
|
renderIcons(element);
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +201,7 @@ export function createParameterControl(param, processorId, idPrefix = 'param') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'button':
|
case 'button':
|
||||||
const buttonText = param.label || 'Click';
|
const buttonText = param.label || 'Нажать';
|
||||||
const actionDesc = opts.action ? `title="${opts.action}"` : '';
|
const actionDesc = opts.action ? `title="${opts.action}"` : '';
|
||||||
return `
|
return `
|
||||||
<div class="chart-setting" data-param="${param.name}">
|
<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()) {
|
export function safeClone(obj, seen = new WeakSet()) {
|
||||||
if (obj === null || typeof obj !== 'object') return obj;
|
if (obj === null || typeof obj !== 'object') return obj;
|
||||||
if (seen.has(obj)) return '[Circular Reference]';
|
if (seen.has(obj)) return '[Циклическая ссылка]';
|
||||||
|
|
||||||
seen.add(obj);
|
seen.add(obj);
|
||||||
|
|
||||||
@ -260,7 +255,7 @@ export function safeClone(obj, seen = new WeakSet()) {
|
|||||||
try {
|
try {
|
||||||
cloned[key] = safeClone(obj[key], seen);
|
cloned[key] = safeClone(obj[key], seen);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cloned[key] = `[Error: ${e.message}]`;
|
cloned[key] = `[Ошибка: ${e.message}]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,11 +303,11 @@ export function escapeHtml(unsafe) {
|
|||||||
* @returns {string} Formatted string
|
* @returns {string} Formatted string
|
||||||
*/
|
*/
|
||||||
export function formatBytes(bytes) {
|
export function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 байт';
|
||||||
|
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
const sizes = ['байт', 'КБ', 'МБ', 'ГБ'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
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]}`;
|
||||||
}
|
}
|
||||||
@ -97,8 +97,8 @@ export class WebSocketManager {
|
|||||||
|
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Connected',
|
title: 'Подключено',
|
||||||
message: 'Real-time connection established'
|
message: 'Установлено соединение в реальном времени'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,8 +109,8 @@ export class WebSocketManager {
|
|||||||
console.error('Error processing WebSocket message:', error);
|
console.error('Error processing WebSocket message:', error);
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Message Error',
|
title: 'Ошибка сообщения',
|
||||||
message: 'Failed to process received data'
|
message: 'Не удалось обработать полученные данные'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -148,8 +148,8 @@ export class WebSocketManager {
|
|||||||
console.error('JSON parse error:', jsonError);
|
console.error('JSON parse error:', jsonError);
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'JSON Parse Error',
|
title: 'Ошибка разбора JSON',
|
||||||
message: `Failed to parse JSON: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
|
message: `Не удалось разобрать JSON: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,8 +175,8 @@ export class WebSocketManager {
|
|||||||
});
|
});
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Server Error',
|
title: 'Ошибка сервера',
|
||||||
message: `${payload.message}${payload.details ? ` - ${payload.details}` : ''}${payload.source ? ` (${payload.source})` : ''}`
|
message: `${payload.message}${payload.details ? ` — ${payload.details}` : ''}${payload.source ? ` (${payload.source})` : ''}`
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -187,8 +187,8 @@ export class WebSocketManager {
|
|||||||
console.log('Raw message:', data);
|
console.log('Raw message:', data);
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Message Processing Error',
|
title: 'Ошибка обработки сообщения',
|
||||||
message: `Error processing message: ${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`
|
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
|
// Only show error notification during connection attempts, not after disconnection
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Connection Failed',
|
title: 'Сбой подключения',
|
||||||
message: 'Unable to establish real-time connection'
|
message: 'Не удалось установить соединение в реальном времени'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,8 +241,8 @@ export class WebSocketManager {
|
|||||||
if (wasConnected) {
|
if (wasConnected) {
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
title: 'Disconnected',
|
title: 'Отключено',
|
||||||
message: 'Real-time connection lost. Attempting to reconnect...'
|
message: 'Соединение потеряно. Пытаемся переподключиться...'
|
||||||
});
|
});
|
||||||
this.scheduleReconnect();
|
this.scheduleReconnect();
|
||||||
}
|
}
|
||||||
@ -263,8 +263,8 @@ export class WebSocketManager {
|
|||||||
console.error('Max reconnection attempts reached');
|
console.error('Max reconnection attempts reached');
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Connection Failed',
|
title: 'Сбой подключения',
|
||||||
message: 'Unable to reconnect after multiple attempts'
|
message: 'Не удалось переподключиться после нескольких попыток'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
8
vna_system/web_ui/static/js/vendor/plotly-2.26.0.min.js
vendored
Normal file
8
vna_system/web_ui/static/js/vendor/plotly-2.26.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="description" content="Панель управления системой ВНА для сбора и обработки данных в реальном времени">
|
||||||
<meta name="keywords" content="VNA, Vector Network Analyzer, RF, Microwave, Data Acquisition, Real-time">
|
<meta name="keywords" content="ВНА, Vector Network Analyzer, РФ, микроволны, сбор данных, реальное время">
|
||||||
<meta name="author" content="VNA System">
|
<meta name="author" content="VNA System">
|
||||||
<title>VNA System Dashboard</title>
|
<title>Система ВНА — Панель управления</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/assets/favicon.svg">
|
||||||
@ -15,15 +15,8 @@
|
|||||||
<!-- Theme color for mobile browsers -->
|
<!-- Theme color for mobile browsers -->
|
||||||
<meta name="theme-color" content="#1e293b">
|
<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 -->
|
<!-- 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 -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||||
@ -39,42 +32,25 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header__container">
|
<div class="header__container">
|
||||||
<div class="header__brand">
|
<div class="header__brand">
|
||||||
<i data-lucide="activity" class="header__icon"></i>
|
<span data-icon="activity" class="header__icon"></span>
|
||||||
<h1 class="header__title">VNA System</h1>
|
<h1 class="header__title">МФТИ</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header__status">
|
<div class="header__status">
|
||||||
<div class="status-indicator" id="connectionStatus">
|
<div class="status-indicator" id="connectionStatus">
|
||||||
<div class="status-indicator__dot"></div>
|
<div class="status-indicator__dot"></div>
|
||||||
<span class="status-indicator__text">Connecting...</span>
|
<span class="status-indicator__text">Подключение...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="header__nav">
|
<nav class="header__nav">
|
||||||
<button class="nav-btn nav-btn--active" data-view="dashboard">
|
<button class="nav-btn nav-btn--active" data-view="dashboard">
|
||||||
<i data-lucide="bar-chart-3"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
<span>Dashboard</span>
|
<span>Панель</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" data-view="settings">
|
<button class="nav-btn" data-view="settings">
|
||||||
<i data-lucide="settings"></i>
|
<span data-icon="settings"></span>
|
||||||
<span>Settings</span>
|
<span>Настройки</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -88,55 +64,72 @@
|
|||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<div class="controls-panel__container">
|
<div class="controls-panel__container">
|
||||||
<div class="controls-group">
|
<div class="controls-group">
|
||||||
<label class="controls-label">Acquisition Control</label>
|
<label class="controls-label">Управление сбором</label>
|
||||||
<div class="acquisition-controls">
|
<div class="acquisition-controls">
|
||||||
<button class="btn btn--primary btn--bordered" id="startBtn" title="Start continuous acquisition">
|
<div class="acquisition-controls__buttons">
|
||||||
<i data-lucide="play"></i>
|
<button class="btn btn--primary" id="startBtn" title="Запустить непрерывный сбор">
|
||||||
Start
|
<span data-icon="play"></span>
|
||||||
</button>
|
Запуск
|
||||||
<button class="btn btn--secondary btn--bordered" id="stopBtn" title="Stop acquisition">
|
</button>
|
||||||
<i data-lucide="square"></i>
|
<button class="btn btn--secondary" id="stopBtn" title="Остановить сбор">
|
||||||
Stop
|
<span data-icon="square"></span>
|
||||||
</button>
|
Стоп
|
||||||
<button class="btn btn--accent btn--bordered" id="singleSweepBtn" title="Trigger single sweep">
|
</button>
|
||||||
<i data-lucide="zap"></i>
|
<button class="btn btn--accent" id="singleSweepBtn" title="Запустить одиночный свип">
|
||||||
Single
|
<span data-icon="zap"></span>
|
||||||
</button>
|
Одиночный
|
||||||
|
</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>
|
||||||
<div class="acquisition-status" id="acquisitionStatus">
|
<div class="acquisition-status" id="acquisitionStatus">
|
||||||
<div class="status-indicator">
|
<div class="status-indicator">
|
||||||
<div class="status-indicator__dot status-indicator__dot--idle"></div>
|
<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>
|
||||||
<div class="acquisition-mode" id="acquisitionMode">
|
<div class="acquisition-mode" id="acquisitionMode">
|
||||||
<span class="mode-label">Mode:</span>
|
<span class="mode-label">Режим:</span>
|
||||||
<span class="mode-value" id="acquisitionModeText">Continuous</span>
|
<span class="mode-value" id="acquisitionModeText">Непрерывный</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls-group">
|
<div class="controls-group">
|
||||||
<label class="controls-label">Processors</label>
|
<label class="controls-label">Процессоры</label>
|
||||||
<div class="processor-toggles" id="processorToggles">
|
<div class="processor-toggles" id="processorToggles">
|
||||||
<!-- Processor toggles will be dynamically generated -->
|
<!-- Переключатели процессоров создаются динамически -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts Grid -->
|
<!-- Charts Grid -->
|
||||||
<div class="charts-container">
|
<div class="charts-container">
|
||||||
<div class="charts-grid" id="chartsGrid">
|
<div class="charts-grid" id="chartsGrid">
|
||||||
<!-- Charts will be dynamically generated -->
|
<!-- Графики создаются динамически -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="empty-state" id="emptyState">
|
<div class="empty-state" id="emptyState">
|
||||||
<div class="empty-state__content">
|
<div class="empty-state__content">
|
||||||
<i data-lucide="activity" class="empty-state__icon"></i>
|
<span data-icon="activity" class="empty-state__icon"></span>
|
||||||
<h3 class="empty-state__title">No Data Yet</h3>
|
<h3 class="empty-state__title">Нет данных</h3>
|
||||||
<p class="empty-state__description">
|
<p class="empty-state__description">
|
||||||
Waiting for sweep data from the VNA system...
|
Ожидаем данные свипа от системы ВНА...
|
||||||
</p>
|
</p>
|
||||||
<div class="loading-spinner">
|
<div class="loading-spinner">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
@ -151,25 +144,25 @@
|
|||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2 class="settings-title">
|
<h2 class="settings-title">
|
||||||
<i data-lucide="settings" class="settings-icon"></i>
|
<span data-icon="settings" class="settings-icon"></span>
|
||||||
VNA Settings
|
Настройки системы
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Configuration Presets Section -->
|
<!-- Configuration Presets Section -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card-title">Configuration Presets</h3>
|
<h3 class="settings-card-title">Конфигурационные пресеты</h3>
|
||||||
<p class="settings-card-description">Select measurement configuration from available presets</p>
|
<p class="settings-card-description">Выберите конфигурацию измерений из доступных пресетов</p>
|
||||||
|
|
||||||
<div class="preset-controls">
|
<div class="preset-controls">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label">Available Presets:</label>
|
<label class="control-label">Доступные пресеты:</label>
|
||||||
<div class="preset-selector" id="presetSelector">
|
<div class="preset-selector" id="presetSelector">
|
||||||
<select class="preset-dropdown settings-select" id="presetDropdown" disabled>
|
<select class="preset-dropdown settings-select" id="presetDropdown" disabled>
|
||||||
<option value="">Loading presets...</option>
|
<option value="">Загрузка пресетов...</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn--primary" id="setPresetBtn" disabled>
|
<button class="btn btn--primary" id="setPresetBtn" disabled>
|
||||||
<i data-lucide="check"></i>
|
<span data-icon="check"></span>
|
||||||
Set Active
|
Сделать активным
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,8 +170,8 @@
|
|||||||
<div class="preset-info" id="presetInfo">
|
<div class="preset-info" id="presetInfo">
|
||||||
<div class="preset-details">
|
<div class="preset-details">
|
||||||
<div class="preset-param">
|
<div class="preset-param">
|
||||||
<span class="param-label">Current:</span>
|
<span class="param-label">Текущий:</span>
|
||||||
<span class="param-value" id="currentPreset">None</span>
|
<span class="param-value" id="currentPreset">Нет</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -187,14 +180,14 @@
|
|||||||
|
|
||||||
<!-- Calibration Section -->
|
<!-- Calibration Section -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card-title">Calibration Management</h3>
|
<h3 class="settings-card-title">Управление калибровкой</h3>
|
||||||
<p class="settings-card-description">Manage calibration data for accurate measurements</p>
|
<p class="settings-card-description">Управление калибровочными данными для точных измерений</p>
|
||||||
|
|
||||||
<!-- Current Calibration Status -->
|
<!-- Current Calibration Status -->
|
||||||
<div class="calibration-status" id="calibrationStatus">
|
<div class="calibration-status" id="calibrationStatus">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">Current Calibration:</span>
|
<span class="status-label">Текущая калибровка:</span>
|
||||||
<span class="status-value" id="currentCalibration">None</span>
|
<span class="status-value" id="currentCalibration">Нет</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -202,18 +195,18 @@
|
|||||||
<div class="calibration-workflow">
|
<div class="calibration-workflow">
|
||||||
<!-- Start New Calibration -->
|
<!-- Start New Calibration -->
|
||||||
<div class="workflow-section">
|
<div class="workflow-section">
|
||||||
<h4 class="workflow-title">New Calibration</h4>
|
<h4 class="workflow-title">Новая калибровка</h4>
|
||||||
<div class="workflow-controls">
|
<div class="workflow-controls">
|
||||||
<button class="btn btn--primary" id="startCalibrationBtn" disabled>
|
<button class="btn btn--primary" id="startCalibrationBtn" disabled>
|
||||||
<i data-lucide="play"></i>
|
<span data-icon="play"></span>
|
||||||
Start Calibration
|
Начать калибровку
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Calibration Steps -->
|
<!-- Calibration Steps -->
|
||||||
<div class="workflow-section" id="calibrationSteps" style="display: none;">
|
<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="calibration-progress" id="calibrationProgress">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span class="progress-text" id="progressText">0/0</span>
|
<span class="progress-text" id="progressText">0/0</span>
|
||||||
@ -221,37 +214,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calibration-standards" id="calibrationStandards">
|
<div class="calibration-standards" id="calibrationStandards">
|
||||||
<!-- Standards buttons will be dynamically generated -->
|
<!-- Кнопки стандартов создаются динамически -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calibration-actions settings-action-group">
|
<div class="calibration-actions settings-action-group">
|
||||||
<button class="btn btn--secondary" id="viewCurrentPlotsBtn" disabled>
|
<button class="btn btn--secondary" id="viewCurrentPlotsBtn" disabled>
|
||||||
<i data-lucide="bar-chart-3"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
View Current Plots
|
Графики текущей калибровки
|
||||||
</button>
|
</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>
|
<button class="btn btn--success" id="saveCalibrationBtn" disabled>
|
||||||
<i data-lucide="save"></i>
|
<span data-icon="save"></span>
|
||||||
Save Calibration
|
Сохранить калибровку
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Existing Calibrations -->
|
<!-- Existing Calibrations -->
|
||||||
<div class="workflow-section">
|
<div class="workflow-section">
|
||||||
<h4 class="workflow-title">Existing Calibrations</h4>
|
<h4 class="workflow-title">Существующие калибровки</h4>
|
||||||
<div class="existing-calibrations" id="existingCalibrations">
|
<div class="existing-calibrations" id="existingCalibrations">
|
||||||
<select class="calibration-dropdown settings-select" id="calibrationDropdown" disabled>
|
<select class="calibration-dropdown settings-select" id="calibrationDropdown" disabled>
|
||||||
<option value="">No calibrations available</option>
|
<option value="">Калибровки отсутствуют</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="calibration-actions settings-action-group">
|
<div class="calibration-actions settings-action-group">
|
||||||
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
|
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
|
||||||
<i data-lucide="check"></i>
|
<span data-icon="check"></span>
|
||||||
Set Active
|
Сделать активной
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn--secondary" id="viewPlotsBtn" disabled>
|
<button class="btn btn--secondary" id="viewPlotsBtn" disabled>
|
||||||
<i data-lucide="bar-chart-3"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
View Plots
|
Показать графики
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -261,75 +254,71 @@
|
|||||||
|
|
||||||
<!-- Open Air Reference -->
|
<!-- Open Air Reference -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card-title">Open Air Reference</h3>
|
<h3 class="settings-card-title">Эталон открытого пространства</h3>
|
||||||
<div class="settings-card-content">
|
<div class="settings-card-content">
|
||||||
|
|
||||||
<!-- Create Reference -->
|
<!-- Create Reference -->
|
||||||
<div class="workflow-section">
|
<div class="workflow-section">
|
||||||
<h4 class="workflow-title">Create Reference</h4>
|
<h4 class="workflow-title">Создать эталон</h4>
|
||||||
<div class="reference-creation">
|
<div class="reference-creation">
|
||||||
<input type="text" class="reference-name-input settings-input" id="referenceNameInput" placeholder="Enter reference name">
|
<input type="text" class="reference-name-input settings-input" id="referenceNameInput" placeholder="Введите имя эталона">
|
||||||
<input type="text" class="reference-description-input settings-input" id="referenceDescriptionInput" placeholder="Description (optional)">
|
<input type="text" class="reference-description-input settings-input" id="referenceDescriptionInput" placeholder="Описание (необязательно)">
|
||||||
<button class="btn btn--primary" id="createReferenceBtn">
|
<button class="btn btn--primary" id="createReferenceBtn">
|
||||||
<i data-lucide="target"></i>
|
<span data-icon="target"></span>
|
||||||
Capture Reference
|
Снять эталон
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Existing References -->
|
<!-- Existing References -->
|
||||||
<div class="workflow-section">
|
<div class="workflow-section">
|
||||||
<h4 class="workflow-title">Existing References</h4>
|
<h4 class="workflow-title">Существующие эталоны</h4>
|
||||||
<div class="existing-references" id="existingReferences">
|
<div class="reference-list" id="referenceList">
|
||||||
<select class="reference-dropdown settings-select" id="referenceDropdown" disabled>
|
<select class="reference-dropdown settings-select" id="referenceDropdown" disabled>
|
||||||
<option value="">No references available</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>
|
||||||
<i data-lucide="check"></i>
|
<span data-icon="check"></span>
|
||||||
Set Active
|
Использовать
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn--secondary" id="clearReferenceBtn" disabled>
|
<button class="btn btn--secondary" id="previewReferenceBtn" disabled>
|
||||||
<i data-lucide="x"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
Clear
|
Просмотр
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn--danger" id="deleteReferenceBtn" disabled>
|
<button class="btn btn--secondary" id="deleteReferenceBtn" disabled>
|
||||||
<i data-lucide="trash-2"></i>
|
<span data-icon="trash-2"></span>
|
||||||
Delete
|
Удалить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Summary -->
|
<!-- System Summary -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card-title">Status Summary</h3>
|
<h3 class="settings-card-title">Сводка системы</h3>
|
||||||
<div class="status-grid" id="statusSummary">
|
<div class="system-summary" id="systemSummary">
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">Available Presets:</span>
|
<span class="status-label">Получено свипов:</span>
|
||||||
<span class="status-value" id="presetCount">-</span>
|
<span class="status-value" id="sweepCount">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item">
|
<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>
|
<span class="status-value" id="calibrationCount">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">System Status:</span>
|
<span class="status-label">Статус системы:</span>
|
||||||
<span class="status-value" id="systemStatus">Checking...</span>
|
<span class="status-value" id="systemStatus">Проверка...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -344,16 +333,16 @@
|
|||||||
<div class="modal__content modal__content--large">
|
<div class="modal__content modal__content--large">
|
||||||
<div class="modal__header">
|
<div class="modal__header">
|
||||||
<h2 class="modal__title">
|
<h2 class="modal__title">
|
||||||
<i data-lucide="bar-chart-3"></i>
|
<span data-icon="bar-chart-3"></span>
|
||||||
Calibration Standards Plots
|
Графики стандартов калибровки
|
||||||
</h2>
|
</h2>
|
||||||
<div class="modal__actions">
|
<div class="modal__actions">
|
||||||
<button class="btn btn--secondary btn--sm" id="downloadAllBtn" title="Download all calibration data">
|
<button class="btn btn--secondary btn--sm" id="downloadAllBtn" title="Скачать все данные калибровки">
|
||||||
<i data-lucide="download-cloud"></i>
|
<span data-icon="download-cloud"></span>
|
||||||
Download All
|
Скачать всё
|
||||||
</button>
|
</button>
|
||||||
<button class="modal__close" data-modal-close title="Close">
|
<button class="modal__close" data-modal-close title="Закрыть">
|
||||||
<i data-lucide="x"></i>
|
<span data-icon="x"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -361,7 +350,7 @@
|
|||||||
<div class="plots-container">
|
<div class="plots-container">
|
||||||
<div class="plots-content">
|
<div class="plots-content">
|
||||||
<div class="plots-grid" id="plotsGrid">
|
<div class="plots-grid" id="plotsGrid">
|
||||||
<!-- Individual plots will be populated here -->
|
<!-- Здесь появятся отдельные графики -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -373,7 +362,6 @@
|
|||||||
<div class="notifications" id="notifications"></div>
|
<div class="notifications" id="notifications"></div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
|
||||||
<script type="module" src="/static/js/main.js"></script>
|
<script type="module" src="/static/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user