Loading noctua/web/pages/control.html +44 −44 Original line number Diff line number Diff line {% extends "base.html" %} {% import "macros/widgets.html" as w %} {% import "macros/sections/control_panel.html" as ctrl %} {% import "macros/sections/mode_panels.html" as mode %} {% import "macros/sections/webcam_panel.html" as webcam %} {% import "macros/sections/synoptic_panel.html" as synoptic %} Loading @@ -20,9 +21,7 @@ "buttons": [{"label": "Set", "endpoint": "/telescope/focuser", "method": "PUT"}], "info_list": [ {"label": "moving", "status": "telescope-focuser-movement"}, {"label": "pos", "status": "telescope-focuser-position", "transform": "round_1"}, {"label": "pos", "status": "telescope-focuser-position", "transform": "round_1"} ] }) }} </div> Loading @@ -32,26 +31,22 @@ </div> </section> <!-- CENTER: Mode Selection & Camera --> <!-- CENTER: Mode selector + per-mode framing & observation forms --> <section class="col-xl-4 col-lg-6"> <!-- The Mode Selector dictates which station/camera is active --> {{ ctrl.observing_mode_selector() }} {{ ctrl.camera_windowing() }} {{ ctrl.observing_mode_selector() }} {{ ctrl.camera_exposure() }} {{ mode.imaging_panel() }} {{ mode.spectro_panel() }} {{ mode.echelle_panel() }} <div class="card bg-black border-secondary p-2 small font-monospace text-success"> <div>Mode: <span id="current-mode-label" class="text-info fw-bold">Imaging</span></div> <div>Camera State: <var data-status="camera-snapshot-state" data-status-suffix="-snapshot-state">N/A</var></div> <div>Cooler: <var data-status="camera-cooler" data-status-suffix="-cooler">N/A</var> (<var data-status="camera-settings-temperature" data-status-suffix="-settings-temperature">N/A</var>°C)</div> </div> </section> <!-- RIGHT: Monitors --> <section class="col-xl-4 col-12"> <div class="card bg-dark border-secondary h-100 shadow-sm"> <div class="card-header p-0 border-secondary d-flex justify-content-between align-items-center"> <div class="card bg-dark border-secondary shadow-sm"> <div class="card-header p-0 border-secondary"> <ul class="nav nav-tabs border-0" role="tablist"> <li class="nav-item"><button class="nav-link active py-2" data-bs-toggle="tab" data-bs-target="#mon-fits">FITS Viewer</button></li> <li class="nav-item"><button class="nav-link py-2" data-bs-toggle="tab" data-bs-target="#mon-webcam">Webcam</button></li> Loading @@ -60,26 +55,31 @@ </ul> <small class="text-muted px-3" id="active-station-id">STATION1</small> </div> <div class="card-body p-0 bg-black position-relative" style="min-height: 500px;"> <div class="tab-content h-100"> <div class="tab-pane fade show active h-100" id="mon-fits"> <div class="tab-content"> <div class="tab-pane fade show active" id="mon-fits"> <div id="fits-viewer-container" class="h-100 overflow-auto p-2"> {{ ctrl.fits_viewer_section('/api') }} </div> </div> <div class="tab-pane fade show active" id="mon-webcam"> <div class="tab-pane fade" id="mon-webcam"> {{ webcam.webcam_panel() }} </div> <div class="tab-pane fade" id="mon-synoptic"> {{ synoptic.synoptic_panel() }} </div> <div class="tab-pane fade" id="mon-output"> <div id="sequencer-output-display" class="p-3 text-info font-monospace small"> Waiting for output data... </div> </div> </div> </div> </div> </section> Loading noctua/web/pages/init.html +22 −17 Original line number Diff line number Diff line Loading @@ -220,14 +220,18 @@ </section> <!-- RIGHT: Monitors --> <section class="col-xl-4 col-lg-12"> <div class="card bg-dark border-secondary shadow-sm"> <div class="card-header p-0 border-secondary"> <ul class="nav nav-tabs border-0" role="tablist"> <li class="nav-item"><button class="nav-link active py-2" data-bs-toggle="tab" data-bs-target="#init-webcam">Webcam</button></li> <li class="nav-item"><button class="nav-link py-2" data-bs-toggle="tab" data-bs-target="#init-synoptic">Synoptic</button></li> </ul> </div> <div class="tab-content"> <div class="tab-pane fade show active" id="init-webcam"> {{ webcam.webcam_panel() }} Loading @@ -236,6 +240,7 @@ {{ synoptic.synoptic_panel() }} </div> </div> </div> </section> Loading noctua/web/static/js/control.js +68 −136 Original line number Diff line number Diff line // control.js // Logic for the Control page: Mode switching, Stage movement, Expose payload. // control.js — Control page: mode switching, stage movement, expose dispatch. import { showToast, setInputState } from './ui.js'; // --------------------------------------------------------------------------- // Mode configuration — single source of truth for per-mode camera behaviour. // To add station3: set enabled=true and fill cameraDevice/cameraPrefix/recenterTarget. // --------------------------------------------------------------------------- const MODE_CONFIG = { station1: { label: 'Imaging', cameraPrefix: '/camera', cameraDevice: 'cam', viewerCameras: ['scicam', 'teccam'], showFilter: true, recenterTarget: 'scicam', template: 'snapshot_imaging', enabled: true, }, station2: { label: 'Spectro', cameraPrefix: '/camera2', cameraDevice: 'cam2', viewerCameras: ['scicam', 'teccam'], showFilter: false, recenterTarget: 'teccam', template: 'snapshot_spectro', enabled: true, }, station3: { label: 'Échelle', cameraPrefix: '/camera3', // cam3 — not yet connected cameraDevice: 'cam3', viewerCameras: ['scicam', 'teccam'], // update when camera3/teccam3 added to viewer.ini showFilter: false, recenterTarget: 'teccam3', // not yet connected template: 'observation', enabled: false, }, // Map station → { panel id suffix, FITS viewer combo, sequencer template, camera device } const MODES = { station1: { panel: 'imaging', combo: 'combo1', template: 'snapshot_imaging', camera: 'cam' }, station2: { panel: 'spectro', combo: 'combo2', template: 'snapshot_spectro', camera: 'cam2' }, station3: { panel: 'echelle', combo: 'combo3', template: null, camera: null, disabled: true }, }; // Framing presets shared across all modes (path is appended to cameraPrefix). const FRAMING_PRESETS = [ { label: 'Full Frame', path: '/frame/full' }, { label: 'Half Frame', path: '/frame/half' }, { label: "Small Frame", path: '/frame/small' }, ]; // --------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { const stationLabel = document.getElementById('active-station-id'); const modeLabel = document.getElementById('current-mode-label'); const framingPresets = document.getElementById('framing-presets'); const rowFilter = document.getElementById('row-filter'); const spanRecenter = document.getElementById('span-recenter-target'); const btnExpose = document.getElementById('btn-camera-expose'); let activeStation = 'station1'; const stationLabel = document.getElementById('active-station-id'); // Disable mode buttons for stations not yet available Object.entries(MODE_CONFIG).forEach(([station, cfg]) => { if (cfg.enabled) return; // ----------------------------------------------------------------------- // Disable radio buttons for unavailable modes // ----------------------------------------------------------------------- Object.entries(MODES).forEach(([station, cfg]) => { if (!cfg.disabled) return; const radio = document.querySelector(`input[name="obs-mode"][value="${station}"]`); const label = radio ? document.querySelector(`label[for="${radio.id}"]`) : null; if (radio) radio.disabled = true; Loading @@ -69,70 +25,29 @@ document.addEventListener('DOMContentLoaded', () => { }); // ----------------------------------------------------------------------- // applyMode — updates all mode-dependent UI regions at once // applyMode — show the right mode panel and FITS viewer combo // ----------------------------------------------------------------------- function applyMode(station) { const cfg = MODE_CONFIG[station]; if (!cfg?.enabled) return; activeStation = station; // 1. Rebuild framing preset buttons with the correct camera prefix if (framingPresets) { framingPresets.innerHTML = ''; FRAMING_PRESETS.forEach(preset => { const btn = document.createElement('button'); btn.className = 'btn btn-outline-primary btn-universal'; btn.dataset.url = `${cfg.cameraPrefix}${preset.path}`; btn.dataset.method = 'PUT'; btn.textContent = preset.label; framingPresets.appendChild(btn); }); } // 2. Show/hide filter selector if (rowFilter) rowFilter.classList.toggle('d-none', !cfg.showFilter); const mode = MODES[station]; if (!mode || mode.disabled) return; // 3. Update recenter camera label if (spanRecenter) spanRecenter.textContent = cfg.recenterTarget; // 4. Update station / mode display labels if (stationLabel) stationLabel.textContent = station.toUpperCase(); if (modeLabel) modeLabel.textContent = cfg.label; // 5. Switch FITS viewer to the matching combo panel switchViewer(station); // 6. Keep expose button aware of active station if (btnExpose) btnExpose.dataset.activeStation = station; // 7. Update data-subsystem on camera cards so dependency-guard can track them const cameraSubsystem = cfg.cameraPrefix.replace(/^\//, '').split('/')[0]; framingPresets?.closest('.card.bg-dark')?.setAttribute('data-subsystem', cameraSubsystem); btnExpose?.closest('.card.bg-dark')?.setAttribute('data-subsystem', cameraSubsystem); // 8. Update data-status on elements whose status key depends on the active camera document.querySelectorAll('[data-status-suffix]').forEach(el => { el.dataset.status = `${cameraSubsystem}${el.dataset.statusSuffix}`; document.querySelectorAll('.mode-panel').forEach(el => { el.classList.toggle('d-none', el.id !== `mode-panel-${mode.panel}`); }); } const comboMap = { station1: 'combo1', station2: 'combo2', station3: 'combo3' }; function switchViewer(station) { const target = comboMap[station]; document.querySelectorAll('[data-combo-view]').forEach(el => { el.classList.toggle('d-none', el.dataset.comboView !== target); el.classList.toggle('d-none', el.dataset.comboView !== mode.combo); }); if (stationLabel) stationLabel.textContent = station.toUpperCase(); } // Mode radio buttons document.querySelectorAll('input[name="obs-mode"]').forEach(radio => { radio.addEventListener('change', e => applyMode(e.target.value)); }); // ----------------------------------------------------------------------- // CHECK target — resolve name/coords and update input in place // CHECK target — resolve name/coords and update input // ----------------------------------------------------------------------- const inputRadec = document.getElementById('in-radec'); const btnCheck = document.getElementById('btn-check-target'); Loading @@ -140,11 +55,9 @@ document.addEventListener('DOMContentLoaded', () => { btnCheck?.addEventListener('click', async () => { const target = inputRadec?.value?.trim(); if (!target) return; const origHtml = btnCheck.innerHTML; btnCheck.disabled = true; btnCheck.innerHTML = '<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>'; try { const res = await fetch('/api/telescope/coordinates/resolve', { method: 'POST', Loading @@ -152,7 +65,6 @@ document.addEventListener('DOMContentLoaded', () => { body: JSON.stringify(target), }); const data = await res.json(); if (data.response) { inputRadec.value = data.response; setInputState(inputRadec, 'valid'); Loading @@ -167,14 +79,13 @@ document.addEventListener('DOMContentLoaded', () => { } }); // Reset border when the user starts typing again inputRadec?.addEventListener('input', () => setInputState(inputRadec, 'reset')); // ----------------------------------------------------------------------- // Tab-aware Slew button: switch URL and inputs when pointing tab changes // Tab-aware Slew button // ----------------------------------------------------------------------- const btnSlew = document.getElementById('btn-mount-slew'); document.getElementById('pointing-tabs')?.addEventListener('shown.bs.tab', (e) => { document.getElementById('pointing-tabs')?.addEventListener('shown.bs.tab', e => { if (!btnSlew) return; const isAltAz = e.target.dataset.bsTarget === '#tab-altaz'; btnSlew.dataset.url = isAltAz ? '/telescope/coordinates/movement/altaz' Loading @@ -187,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => { // ----------------------------------------------------------------------- const inputRel = document.getElementById('stage-rel-val'); const moveStageRelative = async (direction) => { async function moveStageRelative(direction) { const step = parseFloat(inputRel?.value) || 0; const delta = direction * step; try { Loading @@ -202,34 +113,55 @@ document.addEventListener('DOMContentLoaded', () => { } catch { showToast('Stage relative move failed', 'danger'); } }; } document.getElementById('btn-stage-rel-plus') ?.addEventListener('click', () => moveStageRelative(+1)); document.getElementById('btn-stage-rel-minus')?.addEventListener('click', () => moveStageRelative(-1)); // ----------------------------------------------------------------------- // EXPOSE — build sequencer payload from active mode config + form values // EXPOSE helpers // ----------------------------------------------------------------------- if (btnExpose) { btnExpose.addEventListener('mousedown', () => { const cfg = MODE_CONFIG[activeStation]; function readForm(form) { const g = name => form.querySelector(`[name="${name}"]`); const params = { camera: cfg.cameraDevice, recenter_camera: cfg.recenterTarget, object: g('object')?.value || 'test', frametype: g('frametype')?.value || 'Light', exptime: parseFloat(g('exptime')?.value || 10), repeat: parseInt(g('repeat')?.value || 1), binning: parseInt(g('binning')?.value || 1), domeslewing: g('domeslewing')?.checked || false, recenter: g('recenter')?.checked || false, box: parseInt(g('box')?.value || 300), }; const filterEl = g('filter'); if (filterEl) params.filter = filterEl.value; return params; } async function postExpose(template, camera, params) { try { await fetch('/api/sequencer/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template, params: { camera, ...params } }), }); } catch { showToast('Sequencer POST failed', 'danger'); } } document.querySelectorAll('[data-parameter^="observation-"]').forEach(input => { const key = input.dataset.parameter.replace('observation-', ''); if (key === 'filter' && !cfg.showFilter) return; params[key] = (input.type === 'checkbox') ? input.checked : (isNaN(input.value) || input.value === '' ? input.value : parseFloat(input.value)); document.getElementById('btn-imaging-expose')?.addEventListener('click', () => { const form = document.getElementById('form-imaging'); if (form) postExpose('snapshot_imaging', 'cam', readForm(form)); }); btnExpose.dataset.payload = JSON.stringify({ template: cfg.template, params }); document.getElementById('btn-spectro-expose')?.addEventListener('click', () => { const form = document.getElementById('form-spectro'); if (form) postExpose('snapshot_spectro', 'cam2', readForm(form)); }); } // ----------------------------------------------------------------------- // Init // ----------------------------------------------------------------------- applyMode('station1'); }); Loading
noctua/web/pages/control.html +44 −44 Original line number Diff line number Diff line {% extends "base.html" %} {% import "macros/widgets.html" as w %} {% import "macros/sections/control_panel.html" as ctrl %} {% import "macros/sections/mode_panels.html" as mode %} {% import "macros/sections/webcam_panel.html" as webcam %} {% import "macros/sections/synoptic_panel.html" as synoptic %} Loading @@ -20,9 +21,7 @@ "buttons": [{"label": "Set", "endpoint": "/telescope/focuser", "method": "PUT"}], "info_list": [ {"label": "moving", "status": "telescope-focuser-movement"}, {"label": "pos", "status": "telescope-focuser-position", "transform": "round_1"}, {"label": "pos", "status": "telescope-focuser-position", "transform": "round_1"} ] }) }} </div> Loading @@ -32,26 +31,22 @@ </div> </section> <!-- CENTER: Mode Selection & Camera --> <!-- CENTER: Mode selector + per-mode framing & observation forms --> <section class="col-xl-4 col-lg-6"> <!-- The Mode Selector dictates which station/camera is active --> {{ ctrl.observing_mode_selector() }} {{ ctrl.camera_windowing() }} {{ ctrl.observing_mode_selector() }} {{ ctrl.camera_exposure() }} {{ mode.imaging_panel() }} {{ mode.spectro_panel() }} {{ mode.echelle_panel() }} <div class="card bg-black border-secondary p-2 small font-monospace text-success"> <div>Mode: <span id="current-mode-label" class="text-info fw-bold">Imaging</span></div> <div>Camera State: <var data-status="camera-snapshot-state" data-status-suffix="-snapshot-state">N/A</var></div> <div>Cooler: <var data-status="camera-cooler" data-status-suffix="-cooler">N/A</var> (<var data-status="camera-settings-temperature" data-status-suffix="-settings-temperature">N/A</var>°C)</div> </div> </section> <!-- RIGHT: Monitors --> <section class="col-xl-4 col-12"> <div class="card bg-dark border-secondary h-100 shadow-sm"> <div class="card-header p-0 border-secondary d-flex justify-content-between align-items-center"> <div class="card bg-dark border-secondary shadow-sm"> <div class="card-header p-0 border-secondary"> <ul class="nav nav-tabs border-0" role="tablist"> <li class="nav-item"><button class="nav-link active py-2" data-bs-toggle="tab" data-bs-target="#mon-fits">FITS Viewer</button></li> <li class="nav-item"><button class="nav-link py-2" data-bs-toggle="tab" data-bs-target="#mon-webcam">Webcam</button></li> Loading @@ -60,26 +55,31 @@ </ul> <small class="text-muted px-3" id="active-station-id">STATION1</small> </div> <div class="card-body p-0 bg-black position-relative" style="min-height: 500px;"> <div class="tab-content h-100"> <div class="tab-pane fade show active h-100" id="mon-fits"> <div class="tab-content"> <div class="tab-pane fade show active" id="mon-fits"> <div id="fits-viewer-container" class="h-100 overflow-auto p-2"> {{ ctrl.fits_viewer_section('/api') }} </div> </div> <div class="tab-pane fade show active" id="mon-webcam"> <div class="tab-pane fade" id="mon-webcam"> {{ webcam.webcam_panel() }} </div> <div class="tab-pane fade" id="mon-synoptic"> {{ synoptic.synoptic_panel() }} </div> <div class="tab-pane fade" id="mon-output"> <div id="sequencer-output-display" class="p-3 text-info font-monospace small"> Waiting for output data... </div> </div> </div> </div> </div> </section> Loading
noctua/web/pages/init.html +22 −17 Original line number Diff line number Diff line Loading @@ -220,14 +220,18 @@ </section> <!-- RIGHT: Monitors --> <section class="col-xl-4 col-lg-12"> <div class="card bg-dark border-secondary shadow-sm"> <div class="card-header p-0 border-secondary"> <ul class="nav nav-tabs border-0" role="tablist"> <li class="nav-item"><button class="nav-link active py-2" data-bs-toggle="tab" data-bs-target="#init-webcam">Webcam</button></li> <li class="nav-item"><button class="nav-link py-2" data-bs-toggle="tab" data-bs-target="#init-synoptic">Synoptic</button></li> </ul> </div> <div class="tab-content"> <div class="tab-pane fade show active" id="init-webcam"> {{ webcam.webcam_panel() }} Loading @@ -236,6 +240,7 @@ {{ synoptic.synoptic_panel() }} </div> </div> </div> </section> Loading
noctua/web/static/js/control.js +68 −136 Original line number Diff line number Diff line // control.js // Logic for the Control page: Mode switching, Stage movement, Expose payload. // control.js — Control page: mode switching, stage movement, expose dispatch. import { showToast, setInputState } from './ui.js'; // --------------------------------------------------------------------------- // Mode configuration — single source of truth for per-mode camera behaviour. // To add station3: set enabled=true and fill cameraDevice/cameraPrefix/recenterTarget. // --------------------------------------------------------------------------- const MODE_CONFIG = { station1: { label: 'Imaging', cameraPrefix: '/camera', cameraDevice: 'cam', viewerCameras: ['scicam', 'teccam'], showFilter: true, recenterTarget: 'scicam', template: 'snapshot_imaging', enabled: true, }, station2: { label: 'Spectro', cameraPrefix: '/camera2', cameraDevice: 'cam2', viewerCameras: ['scicam', 'teccam'], showFilter: false, recenterTarget: 'teccam', template: 'snapshot_spectro', enabled: true, }, station3: { label: 'Échelle', cameraPrefix: '/camera3', // cam3 — not yet connected cameraDevice: 'cam3', viewerCameras: ['scicam', 'teccam'], // update when camera3/teccam3 added to viewer.ini showFilter: false, recenterTarget: 'teccam3', // not yet connected template: 'observation', enabled: false, }, // Map station → { panel id suffix, FITS viewer combo, sequencer template, camera device } const MODES = { station1: { panel: 'imaging', combo: 'combo1', template: 'snapshot_imaging', camera: 'cam' }, station2: { panel: 'spectro', combo: 'combo2', template: 'snapshot_spectro', camera: 'cam2' }, station3: { panel: 'echelle', combo: 'combo3', template: null, camera: null, disabled: true }, }; // Framing presets shared across all modes (path is appended to cameraPrefix). const FRAMING_PRESETS = [ { label: 'Full Frame', path: '/frame/full' }, { label: 'Half Frame', path: '/frame/half' }, { label: "Small Frame", path: '/frame/small' }, ]; // --------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { const stationLabel = document.getElementById('active-station-id'); const modeLabel = document.getElementById('current-mode-label'); const framingPresets = document.getElementById('framing-presets'); const rowFilter = document.getElementById('row-filter'); const spanRecenter = document.getElementById('span-recenter-target'); const btnExpose = document.getElementById('btn-camera-expose'); let activeStation = 'station1'; const stationLabel = document.getElementById('active-station-id'); // Disable mode buttons for stations not yet available Object.entries(MODE_CONFIG).forEach(([station, cfg]) => { if (cfg.enabled) return; // ----------------------------------------------------------------------- // Disable radio buttons for unavailable modes // ----------------------------------------------------------------------- Object.entries(MODES).forEach(([station, cfg]) => { if (!cfg.disabled) return; const radio = document.querySelector(`input[name="obs-mode"][value="${station}"]`); const label = radio ? document.querySelector(`label[for="${radio.id}"]`) : null; if (radio) radio.disabled = true; Loading @@ -69,70 +25,29 @@ document.addEventListener('DOMContentLoaded', () => { }); // ----------------------------------------------------------------------- // applyMode — updates all mode-dependent UI regions at once // applyMode — show the right mode panel and FITS viewer combo // ----------------------------------------------------------------------- function applyMode(station) { const cfg = MODE_CONFIG[station]; if (!cfg?.enabled) return; activeStation = station; // 1. Rebuild framing preset buttons with the correct camera prefix if (framingPresets) { framingPresets.innerHTML = ''; FRAMING_PRESETS.forEach(preset => { const btn = document.createElement('button'); btn.className = 'btn btn-outline-primary btn-universal'; btn.dataset.url = `${cfg.cameraPrefix}${preset.path}`; btn.dataset.method = 'PUT'; btn.textContent = preset.label; framingPresets.appendChild(btn); }); } // 2. Show/hide filter selector if (rowFilter) rowFilter.classList.toggle('d-none', !cfg.showFilter); const mode = MODES[station]; if (!mode || mode.disabled) return; // 3. Update recenter camera label if (spanRecenter) spanRecenter.textContent = cfg.recenterTarget; // 4. Update station / mode display labels if (stationLabel) stationLabel.textContent = station.toUpperCase(); if (modeLabel) modeLabel.textContent = cfg.label; // 5. Switch FITS viewer to the matching combo panel switchViewer(station); // 6. Keep expose button aware of active station if (btnExpose) btnExpose.dataset.activeStation = station; // 7. Update data-subsystem on camera cards so dependency-guard can track them const cameraSubsystem = cfg.cameraPrefix.replace(/^\//, '').split('/')[0]; framingPresets?.closest('.card.bg-dark')?.setAttribute('data-subsystem', cameraSubsystem); btnExpose?.closest('.card.bg-dark')?.setAttribute('data-subsystem', cameraSubsystem); // 8. Update data-status on elements whose status key depends on the active camera document.querySelectorAll('[data-status-suffix]').forEach(el => { el.dataset.status = `${cameraSubsystem}${el.dataset.statusSuffix}`; document.querySelectorAll('.mode-panel').forEach(el => { el.classList.toggle('d-none', el.id !== `mode-panel-${mode.panel}`); }); } const comboMap = { station1: 'combo1', station2: 'combo2', station3: 'combo3' }; function switchViewer(station) { const target = comboMap[station]; document.querySelectorAll('[data-combo-view]').forEach(el => { el.classList.toggle('d-none', el.dataset.comboView !== target); el.classList.toggle('d-none', el.dataset.comboView !== mode.combo); }); if (stationLabel) stationLabel.textContent = station.toUpperCase(); } // Mode radio buttons document.querySelectorAll('input[name="obs-mode"]').forEach(radio => { radio.addEventListener('change', e => applyMode(e.target.value)); }); // ----------------------------------------------------------------------- // CHECK target — resolve name/coords and update input in place // CHECK target — resolve name/coords and update input // ----------------------------------------------------------------------- const inputRadec = document.getElementById('in-radec'); const btnCheck = document.getElementById('btn-check-target'); Loading @@ -140,11 +55,9 @@ document.addEventListener('DOMContentLoaded', () => { btnCheck?.addEventListener('click', async () => { const target = inputRadec?.value?.trim(); if (!target) return; const origHtml = btnCheck.innerHTML; btnCheck.disabled = true; btnCheck.innerHTML = '<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>'; try { const res = await fetch('/api/telescope/coordinates/resolve', { method: 'POST', Loading @@ -152,7 +65,6 @@ document.addEventListener('DOMContentLoaded', () => { body: JSON.stringify(target), }); const data = await res.json(); if (data.response) { inputRadec.value = data.response; setInputState(inputRadec, 'valid'); Loading @@ -167,14 +79,13 @@ document.addEventListener('DOMContentLoaded', () => { } }); // Reset border when the user starts typing again inputRadec?.addEventListener('input', () => setInputState(inputRadec, 'reset')); // ----------------------------------------------------------------------- // Tab-aware Slew button: switch URL and inputs when pointing tab changes // Tab-aware Slew button // ----------------------------------------------------------------------- const btnSlew = document.getElementById('btn-mount-slew'); document.getElementById('pointing-tabs')?.addEventListener('shown.bs.tab', (e) => { document.getElementById('pointing-tabs')?.addEventListener('shown.bs.tab', e => { if (!btnSlew) return; const isAltAz = e.target.dataset.bsTarget === '#tab-altaz'; btnSlew.dataset.url = isAltAz ? '/telescope/coordinates/movement/altaz' Loading @@ -187,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => { // ----------------------------------------------------------------------- const inputRel = document.getElementById('stage-rel-val'); const moveStageRelative = async (direction) => { async function moveStageRelative(direction) { const step = parseFloat(inputRel?.value) || 0; const delta = direction * step; try { Loading @@ -202,34 +113,55 @@ document.addEventListener('DOMContentLoaded', () => { } catch { showToast('Stage relative move failed', 'danger'); } }; } document.getElementById('btn-stage-rel-plus') ?.addEventListener('click', () => moveStageRelative(+1)); document.getElementById('btn-stage-rel-minus')?.addEventListener('click', () => moveStageRelative(-1)); // ----------------------------------------------------------------------- // EXPOSE — build sequencer payload from active mode config + form values // EXPOSE helpers // ----------------------------------------------------------------------- if (btnExpose) { btnExpose.addEventListener('mousedown', () => { const cfg = MODE_CONFIG[activeStation]; function readForm(form) { const g = name => form.querySelector(`[name="${name}"]`); const params = { camera: cfg.cameraDevice, recenter_camera: cfg.recenterTarget, object: g('object')?.value || 'test', frametype: g('frametype')?.value || 'Light', exptime: parseFloat(g('exptime')?.value || 10), repeat: parseInt(g('repeat')?.value || 1), binning: parseInt(g('binning')?.value || 1), domeslewing: g('domeslewing')?.checked || false, recenter: g('recenter')?.checked || false, box: parseInt(g('box')?.value || 300), }; const filterEl = g('filter'); if (filterEl) params.filter = filterEl.value; return params; } async function postExpose(template, camera, params) { try { await fetch('/api/sequencer/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template, params: { camera, ...params } }), }); } catch { showToast('Sequencer POST failed', 'danger'); } } document.querySelectorAll('[data-parameter^="observation-"]').forEach(input => { const key = input.dataset.parameter.replace('observation-', ''); if (key === 'filter' && !cfg.showFilter) return; params[key] = (input.type === 'checkbox') ? input.checked : (isNaN(input.value) || input.value === '' ? input.value : parseFloat(input.value)); document.getElementById('btn-imaging-expose')?.addEventListener('click', () => { const form = document.getElementById('form-imaging'); if (form) postExpose('snapshot_imaging', 'cam', readForm(form)); }); btnExpose.dataset.payload = JSON.stringify({ template: cfg.template, params }); document.getElementById('btn-spectro-expose')?.addEventListener('click', () => { const form = document.getElementById('form-spectro'); if (form) postExpose('snapshot_spectro', 'cam2', readForm(form)); }); } // ----------------------------------------------------------------------- // Init // ----------------------------------------------------------------------- applyMode('station1'); });