Loading noctua/web/pages/macros/control_panel.html +3 −2 Original line number Diff line number Diff line Loading @@ -414,8 +414,9 @@ <div class="col-md"> <div class="input-group input-group-sm"> <button class="btn btn-primary flex-fill" type="button" id="btn-{{ camera_id }}-expose">Expose</button> <button class="btn btn-primary flex-fill btn-universal" type="button" id="btn-{{ camera_id }}-expose" data-method="POST" data-url="/sequencer/run" data-timeout-ms="0">Expose</button> <button class="btn btn-danger btn-universal" type="button" id="btn-{{ camera_id }}-expose-stop" data-method="DELETE" data-url="/sequencer/run">Stop</button> Loading noctua/web/pages/macros/widgets.html +5 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,11 @@ <!-- WIDGET: Standard --> {% macro widget_standard(config) %} {% set safe_id = config.label | replace(' ', '-') | lower %} {# config.info is unique per widget (it's the status key); config.label often isn't (e.g. "Power"/"Cooling"/"Parked" repeat across devices) — prefer info for the id, falling back to label only for widgets that have none. #} {% set _id_src = config.info.strip('/') | replace('/', '-') if config.info else config.label %} {% set safe_id = _id_src | replace(' ', '-') | lower %} <fieldset class="row mt-1 widget-universal align-items-center" id="input-{{ safe_id }}"> <label class="col-md-2 col-form-label"> {{ config.label }} Loading noctua/web/static/js/actions.js +3 −3 Original line number Diff line number Diff line Loading @@ -15,9 +15,9 @@ document.addEventListener('DOMContentLoaded', () => { const endpoint = button.dataset.url; const safeId = button.dataset.safeId; // data-timeout-ms="0" opts out of the client-side abort timeout for // endpoints that legitimately block server-side for a long time // (e.g. /sequencer/run, which runs synchronously until the whole OB // finishes — see control.js's postExpose() for the same exception). // endpoints that legitimately keep a request open for a long time // (e.g. /sequencer/run, which doesn't respond until the whole OB // finishes — used by both the sequencer's RUN button and Expose). const noTimeout = button.dataset.timeoutMs === '0'; await withBusyButton(button, async () => { Loading noctua/web/static/js/control.js +20 −41 Original line number Diff line number Diff line // control.js — Control page: mode switching, stage movement, expose dispatch. import { showToast, setInputState, withBusyButton, fetchWithTimeout, extractErrorMessage } from './ui.js'; import { setInputState, withBusyButton, fetchWithTimeout } from './ui.js'; import { wireLoopToggle } from './loop-toggle.js'; // Map station → { panel id suffix, FITS viewer combo, sequencer template, Loading Loading @@ -161,51 +161,29 @@ document.addEventListener('DOMContentLoaded', () => { } /** * Starts an exposure via POST /api/sequencer/run. * Keeps a station's Expose button's data-payload in sync with its * form, read right before the shared .btn-universal click handler * (actions.js) picks it up and POSTs to /api/sequencer/run — the * same declarative data-timeout-ms="0" path as the sequencer's RUN * button, since both hit this endpoint for the same reason (see * actions.js's noTimeout comment). * * @param {HTMLButtonElement} btn - the station's Expose button. * @param {string} template - template name (e.g. "snapshot"). * @param {string} camera - device name (e.g. "cam1"). * @param {object} params - readForm()'s output. * @param {string} station - key into MODES (e.g. "station1"). */ async function postExpose(btn, template, camera, params) { await withBusyButton(btn, async () => { try { // No fetchWithTimeout here: /api/sequencer/run blocks server-side // until the whole OB finishes executing (sequencer.py's execute() // runs every template synchronously), which for a real exposure // can legitimately take far longer than the ~12-15s timeout used // everywhere else — aborting this one would kill real exposures. const res = await fetch('/api/sequencer/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template, params: { camera, ...params } }), function wireExpose(station) { const mode = MODES[station]; const btn = document.getElementById(`btn-${mode.panel}-expose`); const form = document.getElementById(`form-${mode.panel}`); if (!btn || !form) return; btn.addEventListener('click', () => { btn.dataset.payload = JSON.stringify({ template: mode.template, params: { camera: mode.camera, ...readForm(form) }, }); if (!res.ok) { const detail = await extractErrorMessage(res, `HTTP ${res.status}`); showToast(`Sequencer error: ${detail}`, 'danger'); } } catch { showToast('Sequencer POST failed', 'danger'); } }); } document.getElementById('btn-imaging-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-imaging'); if (form) postExpose(e.currentTarget, MODES.station1.template, MODES.station1.camera, readForm(form)); }); document.getElementById('btn-spectro-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-spectro'); if (form) postExpose(e.currentTarget, MODES.station2.template, MODES.station2.camera, readForm(form)); }); document.getElementById('btn-echelle-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-echelle'); if (form) postExpose(e.currentTarget, MODES.station3.template, MODES.station3.camera, readForm(form)); }); // Loop switches — one per camera in the whole page, each the sole owner // of that camera's continuous acquisition: // - scicam's switch (in the Expose widget) additionally gates Expose/ Loading Loading @@ -268,6 +246,7 @@ document.addEventListener('DOMContentLoaded', () => { } for (const station of Object.keys(MODES)) { wireExpose(station); wireScicamLoop(station); wireTeccamLoop(station); } Loading Loading
noctua/web/pages/macros/control_panel.html +3 −2 Original line number Diff line number Diff line Loading @@ -414,8 +414,9 @@ <div class="col-md"> <div class="input-group input-group-sm"> <button class="btn btn-primary flex-fill" type="button" id="btn-{{ camera_id }}-expose">Expose</button> <button class="btn btn-primary flex-fill btn-universal" type="button" id="btn-{{ camera_id }}-expose" data-method="POST" data-url="/sequencer/run" data-timeout-ms="0">Expose</button> <button class="btn btn-danger btn-universal" type="button" id="btn-{{ camera_id }}-expose-stop" data-method="DELETE" data-url="/sequencer/run">Stop</button> Loading
noctua/web/pages/macros/widgets.html +5 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,11 @@ <!-- WIDGET: Standard --> {% macro widget_standard(config) %} {% set safe_id = config.label | replace(' ', '-') | lower %} {# config.info is unique per widget (it's the status key); config.label often isn't (e.g. "Power"/"Cooling"/"Parked" repeat across devices) — prefer info for the id, falling back to label only for widgets that have none. #} {% set _id_src = config.info.strip('/') | replace('/', '-') if config.info else config.label %} {% set safe_id = _id_src | replace(' ', '-') | lower %} <fieldset class="row mt-1 widget-universal align-items-center" id="input-{{ safe_id }}"> <label class="col-md-2 col-form-label"> {{ config.label }} Loading
noctua/web/static/js/actions.js +3 −3 Original line number Diff line number Diff line Loading @@ -15,9 +15,9 @@ document.addEventListener('DOMContentLoaded', () => { const endpoint = button.dataset.url; const safeId = button.dataset.safeId; // data-timeout-ms="0" opts out of the client-side abort timeout for // endpoints that legitimately block server-side for a long time // (e.g. /sequencer/run, which runs synchronously until the whole OB // finishes — see control.js's postExpose() for the same exception). // endpoints that legitimately keep a request open for a long time // (e.g. /sequencer/run, which doesn't respond until the whole OB // finishes — used by both the sequencer's RUN button and Expose). const noTimeout = button.dataset.timeoutMs === '0'; await withBusyButton(button, async () => { Loading
noctua/web/static/js/control.js +20 −41 Original line number Diff line number Diff line // control.js — Control page: mode switching, stage movement, expose dispatch. import { showToast, setInputState, withBusyButton, fetchWithTimeout, extractErrorMessage } from './ui.js'; import { setInputState, withBusyButton, fetchWithTimeout } from './ui.js'; import { wireLoopToggle } from './loop-toggle.js'; // Map station → { panel id suffix, FITS viewer combo, sequencer template, Loading Loading @@ -161,51 +161,29 @@ document.addEventListener('DOMContentLoaded', () => { } /** * Starts an exposure via POST /api/sequencer/run. * Keeps a station's Expose button's data-payload in sync with its * form, read right before the shared .btn-universal click handler * (actions.js) picks it up and POSTs to /api/sequencer/run — the * same declarative data-timeout-ms="0" path as the sequencer's RUN * button, since both hit this endpoint for the same reason (see * actions.js's noTimeout comment). * * @param {HTMLButtonElement} btn - the station's Expose button. * @param {string} template - template name (e.g. "snapshot"). * @param {string} camera - device name (e.g. "cam1"). * @param {object} params - readForm()'s output. * @param {string} station - key into MODES (e.g. "station1"). */ async function postExpose(btn, template, camera, params) { await withBusyButton(btn, async () => { try { // No fetchWithTimeout here: /api/sequencer/run blocks server-side // until the whole OB finishes executing (sequencer.py's execute() // runs every template synchronously), which for a real exposure // can legitimately take far longer than the ~12-15s timeout used // everywhere else — aborting this one would kill real exposures. const res = await fetch('/api/sequencer/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template, params: { camera, ...params } }), function wireExpose(station) { const mode = MODES[station]; const btn = document.getElementById(`btn-${mode.panel}-expose`); const form = document.getElementById(`form-${mode.panel}`); if (!btn || !form) return; btn.addEventListener('click', () => { btn.dataset.payload = JSON.stringify({ template: mode.template, params: { camera: mode.camera, ...readForm(form) }, }); if (!res.ok) { const detail = await extractErrorMessage(res, `HTTP ${res.status}`); showToast(`Sequencer error: ${detail}`, 'danger'); } } catch { showToast('Sequencer POST failed', 'danger'); } }); } document.getElementById('btn-imaging-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-imaging'); if (form) postExpose(e.currentTarget, MODES.station1.template, MODES.station1.camera, readForm(form)); }); document.getElementById('btn-spectro-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-spectro'); if (form) postExpose(e.currentTarget, MODES.station2.template, MODES.station2.camera, readForm(form)); }); document.getElementById('btn-echelle-expose')?.addEventListener('click', (e) => { const form = document.getElementById('form-echelle'); if (form) postExpose(e.currentTarget, MODES.station3.template, MODES.station3.camera, readForm(form)); }); // Loop switches — one per camera in the whole page, each the sole owner // of that camera's continuous acquisition: // - scicam's switch (in the Expose widget) additionally gates Expose/ Loading Loading @@ -268,6 +246,7 @@ document.addEventListener('DOMContentLoaded', () => { } for (const station of Object.keys(MODES)) { wireExpose(station); wireScicamLoop(station); wireTeccamLoop(station); } Loading