diff --git a/server/static/app.css b/server/static/app.css index 5c8fa3d..3cea5c2 100644 --- a/server/static/app.css +++ b/server/static/app.css @@ -105,14 +105,53 @@ aside { #controlPanel input, #controlPanel select { width: 100%; padding: 4px; margin-top: 2px; border-radius: 4px; border: none; font-size: 0.8rem; } #controlPanel .cmd-row { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; } #controlPanel .cmd-row button { padding: 4px 8px; font-size: 0.75rem; border: none; border-radius: 4px; cursor: pointer; background: #16213e; color: #eee; } -#macroPresetPanel input, #macroPresetPanel select, #referencePointPanel select { +#referencePointPanel select { width: 100%; padding: 4px; margin-top: 4px; border-radius: 4px; border: none; font-size: 0.8rem; } -#macroPresetPanel .cmd-row { display: flex; gap: 4px; flex-wrap: wrap; } -#macroPresetPanel .cmd-row button, #referencePointPanel button { +#referencePointPanel button { flex: 1; padding: 5px 8px; font-size: 0.75rem; border: none; border-radius: 4px; cursor: pointer; background: #16213e; color: #eee; } +.preset-list { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow: auto; } +.preset-row { + display: grid; grid-template-columns: 18px 1fr; gap: 6px; align-items: center; + padding: 6px; background: #0a0a14; border: 1px solid #333; border-radius: 4px; + cursor: pointer; font-size: 0.8rem; +} +.preset-row:hover, .preset-row.selected { border-color: #4fc3f7; background: #16213e; } +.preset-row.applied .preset-check { color: #00ff88; } +.preset-title { font-weight: 600; } +.preset-desc { color: #aaa; font-size: 0.7rem; margin-top: 2px; } +.preset-actions { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; } +.preset-actions button, .preset-modal-actions button { + flex: 1; padding: 5px 8px; font-size: 0.75rem; border: none; border-radius: 4px; + cursor: pointer; background: #16213e; color: #eee; +} +#btnMacroPresetApply { background: #00ff88; color: #111; font-weight: 600; } +#macroPresetModal { + display: none; position: fixed; inset: 0; z-index: 2500; + background: rgba(0, 0, 0, 0.55); align-items: center; justify-content: center; + padding: 14px; +} +#macroPresetModal.open { display: flex; } +#macroPresetModalCard { + width: min(520px, 100%); max-height: 90vh; overflow: auto; + background: #0f3460; border: 1px solid #444; border-radius: 8px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); +} +#macroPresetModalHeader { + padding: 10px 12px; background: #16213e; display: flex; + justify-content: space-between; align-items: center; font-weight: 600; +} +#btnMacroPresetClose { background: none; border: none; color: #eee; font-size: 1.2rem; cursor: pointer; } +#macroPresetModalBody { padding: 12px; } +#macroPresetModalBody label { display: block; color: #aaa; font-size: 0.75rem; margin-top: 8px; } +#macroPresetModalBody input, #macroPresetModalBody select { + width: 100%; padding: 6px; margin-top: 2px; border-radius: 4px; border: none; font-size: 0.85rem; +} +.preset-edit-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; } +.preset-modal-actions { display: flex; gap: 6px; margin-top: 12px; } +#btnMacroPresetDelete { background: #4a1f2a; } #referencePointSummary { margin-top: 6px; line-height: 1.45; } #referencePointChart { width: 100%; height: 140px; display: block; margin-top: 6px; background: #0a0a14; border-radius: 4px; } .metric-card { background: #0a0a14; border-radius: 4px; padding: 6px; margin-top: 6px; } diff --git a/server/static/app.js b/server/static/app.js index 4c7efb6..4c5d2e5 100644 --- a/server/static/app.js +++ b/server/static/app.js @@ -65,6 +65,8 @@ let dualTracksActive = false; let singleTrackActive = false; let lastDevices = []; let macroPresets = []; +let selectedMacroPresetId = ''; +let appliedMacroPresetId = ''; let referencePointSessions = []; let referencePointMarker = null; const deviceLabelCache = {}; @@ -669,75 +671,178 @@ function paramsFromMacroLines(lines) { return params; } -function selectedMacroPreset() { - const id = document.getElementById('macroPresetSelect')?.value; - return macroPresets.find(p => p.id === id) || null; +function macroLinesFromParams(params) { + const lines = ['S']; + if (params.frequency_hz != null) lines.push(`AT+FQ=${Math.round(Number(params.frequency_hz))}`); + if (params.power_dbm != null) lines.push(`AT+PW=${params.power_dbm}`); + if (params.sf != null) lines.push(`AT+SF=${params.sf}`); + if (params.bw_khz) lines.push(`AT+BW=${params.bw_khz}`); + if (params.code_rate) lines.push(`AT+CR=${params.code_rate}`); + if (params.preamble != null) lines.push(`AT+PL=${params.preamble}`); + if (params.send_timeout_ms != null) lines.push(`AT+TM=${params.send_timeout_ms}`); + if (params.role === 'TX') lines.push('AT+TX'); + if (params.role === 'RX') lines.push('AT+RX'); + return lines; } -function updateMacroPresetSelect(preserveId) { - const sel = document.getElementById('macroPresetSelect'); - if (!sel) return; - const prev = preserveId ?? sel.value; - sel.innerHTML = ''; - macroPresets.forEach(p => { - const opt = document.createElement('option'); - opt.value = p.id; - opt.textContent = p.name || p.id; - sel.appendChild(opt); - }); - if (prev) sel.value = prev; +function numOrNull(id) { + const value = document.getElementById(id)?.value; + if (value == null || String(value).trim() === '') return null; + const n = Number(value); + return Number.isFinite(n) ? n : null; +} + +function buildPresetEditParams() { + const params = {}; + const fq = numOrNull('presetEditFq'); + if (fq != null) params.frequency_hz = Math.round(fq * 1e6); + const pw = numOrNull('presetEditPw'); + if (pw != null) params.power_dbm = pw; + const sf = numOrNull('presetEditSf'); + if (sf != null) params.sf = sf; + const pl = numOrNull('presetEditPl'); + if (pl != null) params.preamble = pl; + const tm = numOrNull('presetEditTm'); + if (tm != null) params.send_timeout_ms = tm; + const bw = document.getElementById('presetEditBw')?.value; + if (bw) params.bw_khz = bw; + const cr = document.getElementById('presetEditCr')?.value; + if (cr) params.code_rate = cr; + const role = document.getElementById('presetEditRole')?.value; + if (role) params.role = role; + return params; +} + +function fillPresetModal(preset) { + const params = macroPresetParams(preset); + document.getElementById('presetEditId').value = preset?.id || ''; + document.getElementById('presetEditName').value = preset?.name || ''; + document.getElementById('presetEditDescription').value = preset?.description || ''; + document.getElementById('presetEditFq').value = + params.frequency_hz != null ? (Number(params.frequency_hz) / 1e6).toFixed(3) : ''; + document.getElementById('presetEditPw').value = params.power_dbm ?? ''; + document.getElementById('presetEditSf').value = params.sf ?? ''; + document.getElementById('presetEditPl').value = params.preamble ?? ''; + document.getElementById('presetEditTm').value = params.send_timeout_ms ?? ''; + document.getElementById('presetEditBw').value = params.bw_khz ?? ''; + document.getElementById('presetEditCr').value = params.code_rate ?? ''; + document.getElementById('presetEditRole').value = params.role ?? ''; + document.getElementById('btnMacroPresetDelete').style.display = preset ? '' : 'none'; + document.getElementById('macroPresetModalTitle').textContent = + preset ? 'Редактировать пресет' : 'Создать пресет'; +} + +function setMacroPresetModalOpen(open) { + document.getElementById('macroPresetModal')?.classList.toggle('open', open); +} + +function openMacroPresetCreate() { + fillPresetModal(null); + setMacroPresetModalOpen(true); + document.getElementById('presetEditName')?.focus(); +} + +function openMacroPresetEdit() { + const preset = selectedMacroPreset(); + if (!preset) { + const status = document.getElementById('macroPresetStatus'); + if (status) status.textContent = 'Выберите пресет для редактирования'; + return; + } + fillPresetModal(preset); + setMacroPresetModalOpen(true); + document.getElementById('presetEditName')?.focus(); } async function loadMacroPresets() { const res = await fetch('/api/macro-presets?include_disabled=true', { cache: 'no-store' }); if (!res.ok) throw new Error('macro presets ' + res.status); macroPresets = await res.json(); - updateMacroPresetSelect(); + if (selectedMacroPresetId && !macroPresets.some(p => p.id === selectedMacroPresetId)) { + selectedMacroPresetId = ''; + } + renderMacroPresetList(); const status = document.getElementById('macroPresetStatus'); if (status) status.textContent = `${macroPresets.length} пресет(ов)`; } -function fillMacroPresetEditor(preset) { - document.getElementById('macroPresetName').value = preset?.name || ''; - document.getElementById('macroPresetDescription').value = preset?.description || ''; +function selectedMacroPreset() { + return macroPresets.find(p => p.id === selectedMacroPresetId) || null; } -function applySelectedMacroPresetToCommandForm() { +function renderMacroPresetList() { + const list = document.getElementById('macroPresetList'); + if (!list) return; + if (!macroPresets.length) { + list.innerHTML = '