Commit 7c385fb9 authored by vertighel's avatar vertighel
Browse files

feat: guider-panel.js; disabled field styling



- guider-panel.js: auto-discovering panel init, camera→exptime reactivity,
  target→custom-coords reactivity, actuator translation (ao_x_offload→ao_x+ao_offload),
  Guide button POST to /api/guider/ with spinner
- style.css: label:has(~ div :disabled) + .form-control/select:disabled → text-muted color
- control.html: guider-panel.js script tag added

Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent fd3fba1d
Loading
Loading
Loading
Loading
+8 −7
Original line number Diff line number Diff line
@@ -156,11 +156,6 @@
      
      <h4 class="mt-2 mb-2">Guider</h4>

      {{ ctrl.guider_panel("imaging",
                           [("cam","scicam"), ("teccam","teccam")],
                           "camera",
                           has_actuator_select=True) }}

    </section>

    <section id="mode-panel-spectro" class="mode-panel d-none" data-subsystem="camera2">
@@ -175,12 +170,17 @@
      
      <h4 class="mt-2 mb-2">Guider</h4>

    </section>

      {{ ctrl.guider_panel("imaging",
                           [("cam","scicam"), ("teccam","teccam")],
                           "camera",
                           has_actuator_select=True) }}

      {{ ctrl.guider_panel("spectro",
                           [("teccam2","teccam2")],
                           "camera2") }}

    </section>

  </article>

  <!-- RIGHT: Monitors -->
@@ -229,6 +229,7 @@

{% block scripts %}
    <script type="module" src="{{ url_for('web.static', filename='js/control.js') }}"></script>
    <script type="module" src="{{ url_for('web.static', filename='js/guider-panel.js') }}"></script>
    <script type="module">
    import { FitsViewer } from "{{ url_for('web.static', filename='js/viewer/fits-viewer.js') }}";

+15 −0
Original line number Diff line number Diff line
@@ -129,3 +129,18 @@ var {
    user-select: none;
    transition: opacity 0.3s ease;
}


/* ── 7. Disabled field styling ─────────────────────────────────────────── */

/* Mute the label that directly precedes a disabled input or select.
   The ~ combinator scopes the match to the label before that specific div,
   leaving adjacent labels for still-active fields unaffected. */
label:has(~ div :disabled) {
    color: var(--bs-secondary-color);
}

.form-control:disabled,
.form-select:disabled {
    color: var(--bs-secondary-color);
}
+177 −0
Original line number Diff line number Diff line
// guider-panel.js
// Guider panel UI — camera/exptime reactivity, target coords, Guide button dispatch.
//
// Auto-discovers panel instances from the DOM: any element whose id matches
// "btn-{panelId}-guide-start" triggers initialisation of that panel.

import { showToast } from './ui.js';

// --- Constants ---

/** Camera device names that acquire passively (read FITS from disk, no trigger). */
const PASSIVE_CAMERAS = new Set(['cam', 'cam2']);

/**
 * Maps the three-way UI actuator choice to the API params sent to /guider/.
 * "ao_x_offload" is a UI convenience: backend sees actuator="ao_x" + ao_offload=true.
 */
const ACTUATOR_PARAMS = {
    telescope:    { actuator: 'telescope'               },
    ao_x:         { actuator: 'ao_x', ao_offload: false },
    ao_x_offload: { actuator: 'ao_x', ao_offload: true  },
};

// --- 1. Camera → exptime ---

/**
 * Disables and clears the exptime input for passive cameras (scicam reads FITS
 * from disk — exptime is irrelevant and could mislead the operator).
 * Re-enables it when an active (triggering) camera is selected.
 *
 * @param {string} panelId - Panel identifier ("imaging" or "spectro").
 * @param {string} camera  - Selected camera device name.
 */
function onCameraChange(panelId, camera) {
    const el = document.getElementById(`guider-exptime-val-${panelId}`);
    if (!el) return;

    const passive = PASSIVE_CAMERAS.has(camera);
    el.disabled = passive;
    if (passive) el.value = '';
}

// --- 2. Target → custom coords ---

/**
 * Enables the TX/TY coordinate inputs when target mode is "custom".
 * Clears and disables them for "center" and "stick".
 *
 * @param {string} panelId - Panel identifier.
 * @param {string} target  - Selected target mode ("center", "stick", or "custom").
 */
function onTargetChange(panelId, target) {
    const tx = document.getElementById(`guider-tx-${panelId}`);
    const ty = document.getElementById(`guider-ty-${panelId}`);
    if (!tx || !ty) return;

    const custom = target === 'custom';
    tx.disabled = !custom;
    ty.disabled = !custom;
    if (!custom) {
        tx.value = '';
        ty.value = '';
    }
}

// --- 3. Collect + POST ---

/**
 * Reads all inputs in a guider panel and returns a params object
 * ready to POST to /api/guider/.
 *
 * Target handling:
 *   "center" / "stick" → sent as-is.
 *   "custom"           → sent as [tx, ty] pixel array if both fields are filled.
 *
 * Actuator handling:
 *   "ao_x_offload" is translated to { actuator: "ao_x", ao_offload: true }.
 *
 * @param {string} panelId - Panel identifier.
 * @returns {object} Params dict for the guider API.
 */
function collectParams(panelId) {
    const get = id => document.getElementById(id);

    const camera      = get(`guider-camera-${panelId}`)?.value ?? 'teccam';
    const actuatorRaw = get(`guider-actuator-sel-${panelId}`)?.value ?? 'telescope';
    const interval    = parseFloat(get(`guider-interval-val-${panelId}`)?.value) || 5.0;
    const box         = parseInt(get(`guider-box-val-${panelId}`)?.value)        || 256;

    const cxRaw = get(`guider-cx-${panelId}`)?.value;
    const cyRaw = get(`guider-cy-${panelId}`)?.value;
    const cx    = cxRaw !== '' ? parseInt(cxRaw) : null;
    const cy    = cyRaw !== '' ? parseInt(cyRaw) : null;

    const targetSel = get(`guider-target-${panelId}`)?.value ?? 'center';
    let target = targetSel;
    if (targetSel === 'custom') {
        const tx = parseFloat(get(`guider-tx-${panelId}`)?.value);
        const ty = parseFloat(get(`guider-ty-${panelId}`)?.value);
        if (!isNaN(tx) && !isNaN(ty)) target = [tx, ty];
    }

    const params = {
        camera,
        ...( ACTUATOR_PARAMS[actuatorRaw] ?? ACTUATOR_PARAMS.telescope ),
        interval,
        box,
        target,
    };

    if (!PASSIVE_CAMERAS.has(camera)) {
        const exptime = parseFloat(get(`guider-exptime-val-${panelId}`)?.value);
        if (!isNaN(exptime)) params.exptime = exptime;
    }

    if (cx !== null && cy !== null) params.box_center = [cx, cy];

    return params;
}

/**
 * POSTs guider params to /api/guider/, showing a spinner on the button while pending.
 *
 * @param {HTMLButtonElement} btn    - The Guide button element.
 * @param {object}            params - Params from collectParams().
 */
async function postGuider(btn, params) {
    const orig     = btn.innerHTML;
    btn.disabled   = true;
    btn.innerHTML  = '<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>';

    try {
        const res = await fetch('/api/guider/', {
            method:  'POST',
            headers: { 'Content-Type': 'application/json' },
            body:    JSON.stringify(params),
        });
        if (!res.ok) {
            const err = await res.json().catch(() => ({}));
            throw new Error(err.error || 'Server error');
        }
    } catch (err) {
        showToast(`Guider: ${err.message}`, 'danger');
    } finally {
        btn.disabled  = false;
        btn.innerHTML = orig;
    }
}

// --- 4. Init ---

/**
 * Wires all event listeners for one guider panel instance and applies
 * the initial UI state (exptime/coords may already be disabled on load).
 *
 * @param {string} panelId - Panel identifier derived from the Guide button ID.
 */
function initPanel(panelId) {
    const cameraEl = document.getElementById(`guider-camera-${panelId}`);
    const targetEl = document.getElementById(`guider-target-${panelId}`);
    const guideBtn = document.getElementById(`btn-${panelId}-guide-start`);

    cameraEl?.addEventListener('change', e => onCameraChange(panelId, e.target.value));
    targetEl?.addEventListener('change', e => onTargetChange(panelId, e.target.value));
    guideBtn?.addEventListener('click',  () => postGuider(guideBtn, collectParams(panelId)));

    // Apply initial disabled state derived from current select values
    if (cameraEl) onCameraChange(panelId, cameraEl.value);
    if (targetEl) onTargetChange(panelId, targetEl.value);
}

document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('[id$="-guide-start"]').forEach(btn => {
        const panelId = btn.id.replace(/^btn-/, '').replace(/-guide-start$/, '');
        initPanel(panelId);
    });
});