Commit b4f2368c authored by vertighel's avatar vertighel
Browse files

Fase 5 JS punto 9 (ultimo): JSDoc sui file che ne erano privi



- Nessuno dei 10 file listati nel documento delle convenzioni esporta
  funzioni (sono script di pagina auto-eseguiti, non moduli
  riusabili) -- "exported" letto come "funzioni nominate significative
  di primo livello", documentate: control.js (8), sequencer.js (14),
  synoptic.js (8), status-view.js (1, makeVar), dependency-guard.js
  (3), ws-client.js (3), actions.js (1, updateBadgeValue).
  status-stream.js/toggle.js/webcam.js non hanno funzioni nominate di
  primo livello, niente da aggiungere.
- Tradotti 2 commenti italiani in ws-client.js trovati mentre si
  aggiungeva JSDoc allo stesso file.
- Verificato con un node --check sweep completo di tutti i .js del
  progetto, non solo i file di questo punto.
- PLAN.md aggiornato: fase 5 (JavaScript) chiusa per intero, tutti e 9
  i punti.

Co-Authored-By: default avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 9652fee5
Loading
Loading
Loading
Loading
+26 −3
Original line number Diff line number Diff line
@@ -615,8 +615,26 @@ Working point by point per the user's request, checking in after each
      imported elsewhere — dropped `export`, kept the
      const/function. Verified with `node --check` across every JS
      file touched so far this phase, not just this point's files.
- [ ] Add JSDoc to exported functions in the files that have none,
      following the `viewer/` subfolder as the model.
- [x] Add JSDoc to exported functions in the files that have none,
      following the `viewer/` subfolder as the model. None of the 10
      listed files (`control.js`, `sequencer.js`, `synoptic.js`,
      `status-view.js`, `status-stream.js`, `dependency-guard.js`,
      `toggle.js`, `ws-client.js`, `actions.js`, `webcam.js`) actually
      `export` anything — they're all self-executing page scripts, not
      reusable modules — so read "exported" as "the file's own
      significant top-level named functions" and documented those:
      8 in `control.js`, 14 in `sequencer.js`, 8 in `synoptic.js`, 1 in
      `status-view.js` (`makeVar`), 3 in `dependency-guard.js`, 3 in
      `ws-client.js`, 1 in `actions.js` (`updateBadgeValue`).
      `status-stream.js`, `toggle.js`, `webcam.js` have zero named
      top-level functions (single inline listener each, already
      commented inline) — nothing to add. Translated 2 stray Italian
      comments in `ws-client.js` found while adding its JSDoc (not a
      checklist item for JS specifically, but matches the pattern from
      earlier phases). Verified with a full `node --check` sweep of
      every `.js` file in the project, not just this point's files.

**Phase 5 (JavaScript) done — all 9 points.**

### 6. Cross-cutting UX-linearity pass

@@ -649,4 +667,9 @@ Phase 4 (Jinja/HTML) partly done: `<var>`→`<data>` rename and repo
cruft cleanup closed. Sass migration (its own sub-phase per the
blast-radius warning) deferred, not started.

**Phase 5 (JavaScript) not started.**
Phase 5 (JavaScript) done, full scope, all 9 points worked through
one at a time. Two dead-CSS-selector bugs found and fixed along the
way (`.cv-principale``.cv-main`); `.cv-teccam` flagged but left
alone (separate decision needed).

**Phase 6 (Cross-cutting UX-linearity pass) not started.**
+9 −0
Original line number Diff line number Diff line
@@ -153,6 +153,15 @@ document.addEventListener('DOMContentLoaded', () => {
        }
    });

    /**
     * Applies an immediate local visual update to a widget's badge/raw
     * elements after a successful action, without waiting for the next
     * telemetry broadcast.
     *
     * @param {string} safeId - widget's data-safe-id.
     * @param {*} response - formatted value for the badge.
     * @param {*} [raw] - raw value for the raw-view element, defaults to response.
     */
    function updateBadgeValue(safeId, response, raw) {
        const badge = document.getElementById(`badge-${safeId}`);
        const rawVar = document.getElementById(`var-${safeId}`);
+71 −28
Original line number Diff line number Diff line
@@ -27,19 +27,21 @@ document.addEventListener('DOMContentLoaded', () => {
        }
    });

    // -----------------------------------------------------------------------
    // Viewer "Auto" flags — two-dimensional: mode (imaging/spectro/echelle,
    // i.e. which combo) × Monitor tab (Scicam #mon-fits / Teccam #mon-teccam,
    // i.e. which role). Exactly one of the 6 Auto checkboxes is ever on: the
    // one for the active combo *and* the active Monitor tab. Every other one
    // is off and disabled — a viewer left auto-refreshing while its combo or
    // its role tab isn't the one being looked at just burns cycles/bandwidth
    // on frames nobody sees. The viewer's own listener (fits-viewer.js) picks
    // up the synthetic 'change' event and flips its internal autoUpdate state.
    // -----------------------------------------------------------------------
    let currentStation    = 'station1';
    let currentViewerRole = 'sci';   // 'sci' | 'tec' — #mon-fits starts active

    /**
     * Syncs the 6 "Auto" viewer checkboxes to the current mode × Monitor
     * tab. Two-dimensional: mode (imaging/spectro/echelle, i.e. which
     * combo) × Monitor tab (Scicam #mon-fits / Teccam #mon-teccam, i.e.
     * which role). Exactly one of the 6 is ever on: the one for the
     * active combo *and* the active Monitor tab. Every other one is off
     * and disabled — a viewer left auto-refreshing while its combo or
     * role tab isn't the one being looked at just burns cycles/bandwidth
     * on frames nobody sees. The viewer's own listener (fits-viewer.js)
     * picks up the synthetic 'change' event and flips its internal
     * autoUpdate state.
     */
    function updateViewerAutoFlags() {
        const activeCombo = MODES[currentStation]?.combo;
        for (const mode of Object.values(MODES)) {
@@ -64,10 +66,14 @@ document.addEventListener('DOMContentLoaded', () => {
        updateViewerAutoFlags();
    });

    // -----------------------------------------------------------------------
    // applyMode — the mode tab-pane itself is shown/hidden by Bootstrap's own
    // tab JS (data-bs-toggle="tab"); this only handles the side effects.
    // -----------------------------------------------------------------------
    /**
     * Applies the side effects of switching to a station's mode. The
     * mode tab-pane itself is shown/hidden by Bootstrap's own tab JS
     * (data-bs-toggle="tab"); this only handles what Bootstrap doesn't:
     * viewer auto-flags, combo-scoped element visibility, station label.
     *
     * @param {string} station - key into MODES (e.g. "station1").
     */
    function applyMode(station) {
        const mode = MODES[station];
        if (!mode || mode.disabled) return;
@@ -136,10 +142,17 @@ document.addEventListener('DOMContentLoaded', () => {
    const btnStageRelPlus  = document.getElementById('btn-stage-rel-plus');
    const btnStageRelMinus = document.getElementById('btn-stage-rel-minus');

    // Disabling only the clicked button isn't enough here: this is a
    // read-then-write (GET position, PUT computed from it), so the +
    // and - buttons must also lock each other out, not just themselves,
    // or a quick +/- pair could race on the same stale read.
    /**
     * Moves the stage by +/- the value in #stage-rel-val. Disabling
     * only the clicked button isn't enough here: this is a
     * read-then-write (GET position, PUT computed from it), so the +
     * and - buttons also lock each other out, not just themselves, or
     * a quick +/- pair could race on the same stale read.
     *
     * @param {HTMLButtonElement} btn - the clicked button (gets the spinner).
     * @param {HTMLButtonElement} otherBtn - its sibling (just disabled).
     * @param {number} direction - +1 or -1.
     */
    async function moveStageRelative(btn, otherBtn, direction) {
        const step  = parseFloat(inputRel?.value) || 0;
        const delta = direction * step;
@@ -170,6 +183,13 @@ document.addEventListener('DOMContentLoaded', () => {
    // -----------------------------------------------------------------------
    // EXPOSE helpers
    // -----------------------------------------------------------------------
    /**
     * Reads the Expose params out of a form (looking both inside it and
     * for elements elsewhere on the page tied to it via form="...").
     *
     * @param {HTMLFormElement} form
     * @returns {object} object, imagetype, exptime, repeat, binning, filter?.
     */
    function readForm(form) {
        const g = name => form.querySelector(`[name="${name}"]`)
                       ?? document.querySelector(`[name="${name}"][form="${form.id}"]`);
@@ -185,6 +205,14 @@ document.addEventListener('DOMContentLoaded', () => {
        return params;
    }

    /**
     * Starts an exposure via POST /api/sequencer/run.
     *
     * @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.
     */
    async function postExpose(btn, template, camera, params) {
        await withBusyButton(btn, async () => {
            try {
@@ -223,7 +251,6 @@ document.addEventListener('DOMContentLoaded', () => {
        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/
@@ -233,7 +260,13 @@ document.addEventListener('DOMContentLoaded', () => {
    //   - teccam's switch (in teccam_panel) is the *only* way to acquire on
    //     a teccam — the guider never triggers it (guider.py._acquire()
    //     only ever reads the last frame from disk).
    // -----------------------------------------------------------------------

    /**
     * Wires a station's scicam Loop switch, gating its Expose/Stop
     * buttons on the loop state.
     *
     * @param {string} station - key into MODES (e.g. "station1").
     */
    function wireScicamLoop(station) {
        const mode = MODES[station];
        const exposeBtn = document.getElementById(`btn-${mode.panel}-expose`);
@@ -252,11 +285,17 @@ document.addEventListener('DOMContentLoaded', () => {
        );
    }

    // Gain — absent from teccam1's panel (has_gain=False, STX guider has no
    // gain concept); the lookup below is then NaN and omitted from the body.
    // Passive like Exptime/Binning: read only at the moment the switch turns
    // on, never applied live (a live PUT racing the loop's own connection
    // setup caused intermittent VmbPy "outside of 'with' context" errors).
    /**
     * Wires a station's teccam Loop switch — the only way to acquire on
     * that teccam. Gain is absent from teccam1's panel (has_gain=False,
     * STX guider has no gain concept); the lookup below is then NaN and
     * omitted from the body. Gain/exptime/binning are passive like
     * everywhere else: read only at the moment the switch turns on,
     * never applied live (a live PUT racing the loop's own connection
     * setup caused intermittent VmbPy "outside of 'with' context" errors).
     *
     * @param {string} station - key into MODES (e.g. "station1").
     */
    function wireTeccamLoop(station) {
        const mode = MODES[station];
        wireLoopToggle(
@@ -301,9 +340,13 @@ document.addEventListener('DOMContentLoaded', () => {
    // -----------------------------------------------------------------------
    applyMode('station1');

    // -----------------------------------------------------------------------
    // Pick mode: outline danger on the viewer tab that owns the active viewer
    // -----------------------------------------------------------------------
    /**
     * Resolves the Monitor tab button that owns a given viewer id, for
     * outlining it while a guider pick is active on that viewer.
     *
     * @param {string|null} viewerId - e.g. "scicam1-sci" or "teccam2-tec".
     * @returns {HTMLElement|null}
     */
    function tabBtnForViewer(viewerId) {
        if (!viewerId) return null;
        if (viewerId.endsWith('-sci')) return document.querySelector('[data-bs-target="#mon-fits"]');
+26 −0
Original line number Diff line number Diff line
@@ -26,10 +26,27 @@ document.addEventListener('endpoint-roots', e => {
    Object.assign(endpointGates, gates);
});

/**
 * Finds the widget container a button belongs to, for auto-detected
 * (non-`data-subsystem`) containers.
 *
 * @param {HTMLElement} el - a `.btn-universal`.
 * @returns {HTMLElement|null}
 */
function nearestContainer(el) {
    return el.closest('fieldset.widget-universal, .card.bg-dark');
}

/**
 * True if `container` holds the subsystem's own root/power button,
 * and that root is reachable (or roots haven't loaded yet) — meaning
 * it must stay clickable even while the rest of the subsystem is
 * marked offline, so the user can turn it back on.
 *
 * @param {HTMLElement} container
 * @param {string} sub - subsystem name (key into ROOTS).
 * @returns {boolean}
 */
function isRootContainer(container, sub) {
    const rootPath = ROOTS[sub];
    if (!container.querySelector(`.btn-universal[data-url="${rootPath}"]`)) return false;
@@ -47,6 +64,15 @@ function isRootContainer(container, sub) {
    return gate ? endpointStates[gate] === true : false;
}

/**
 * Toggles `.subsystem-offline` on every container belonging to a
 * subsystem — both explicit `[data-subsystem]` ones and auto-detected
 * ones (containers holding a `/{sub}/...` button) — exempting the
 * subsystem's own root/power container so it stays usable.
 *
 * @param {string} sub - subsystem name (key into ROOTS).
 * @param {boolean} offline
 */
function applyGuard(sub, offline) {
    const rootPath = ROOTS[sub];

+68 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ document.addEventListener('DOMContentLoaded', () => {
    let schemaCache = {}; 
    let dragSrcEl = null;

    /** Writes every rendered form input's current value back into currentOB. */
    function syncFromForm() {
        const inputs = formContainer.querySelectorAll('[data-parameter]');
        inputs.forEach(input => {
@@ -50,6 +51,14 @@ document.addEventListener('DOMContentLoaded', () => {

    // --- 1. Browser Logic (Sorted Alphabetically) ---

    /**
     * Refetches and re-renders one browser list (templates or blocks),
     * alphabetically sorted.
     *
     * @param {'templates'|'blocks'} type
     * @param {HTMLButtonElement} [btn] - refresh button that triggered this,
     *   if any (undefined for the init-time/post-save/post-delete calls).
     */
    async function refreshBrowser(type, btn) {
        await withBusyButton(btn, async () => {
            try {
@@ -98,6 +107,13 @@ document.addEventListener('DOMContentLoaded', () => {

    // --- 2. Saving and Loading ---

    /**
     * Saves currentOB (or the raw-editor JSON, if that's the active view)
     * as an OB block under the given name.
     *
     * @param {string} name
     * @param {HTMLButtonElement} [btn] - triggering button, if any.
     */
    async function saveOB(name, btn) {
        if (toggleRaw.checked) {
            try {
@@ -132,6 +148,12 @@ document.addEventListener('DOMContentLoaded', () => {
        });
    }

    /**
     * Deletes an OB block after a confirm() prompt.
     *
     * @param {string} name
     * @param {HTMLButtonElement} [btn] - triggering button, if any.
     */
    async function deleteOB(name, btn) {
        if (!confirm(`Delete OB ${name}?`)) return;
        await withBusyButton(btn, async () => {
@@ -151,6 +173,12 @@ document.addEventListener('DOMContentLoaded', () => {

    // --- 3. Step Manipulation ---

    /**
     * Appends a new step to currentOB using a template's default params,
     * then re-renders.
     *
     * @param {string} tplName - template name (e.g. "snapshot").
     */
    async function appendStep(tplName) {
        try {
            const tplContent = await fetch(`/api/templates/${tplName}`).then(r => r.json());
@@ -163,6 +191,11 @@ document.addEventListener('DOMContentLoaded', () => {
        }
    }

    /**
     * Removes a step from currentOB by index, then re-renders.
     *
     * @param {number} index
     */
    function removeStep(index) {
        currentOB.splice(index, 1);
        renderEditor();
@@ -170,6 +203,11 @@ document.addEventListener('DOMContentLoaded', () => {

    // --- 4. Rendering Engine ---

    /**
     * Rebuilds #editor-form-container from currentOB (or just syncs the
     * raw textarea, if that's the active view). Shows the "no steps"
     * placeholder when currentOB is empty.
     */
    async function renderEditor() {
        if (toggleRaw.checked) {
            rawTextArea.value = JSON.stringify(currentOB, null, 4);
@@ -210,6 +248,17 @@ document.addEventListener('DOMContentLoaded', () => {
        btnRun.dataset.payload = JSON.stringify(currentOB);
    }

    /**
     * Clones the right `tpl-input-*` blueprint for one param (single,
     * boolean, or array) and wires it to syncFromForm() on change.
     *
     * @param {number} stepIdx - index into currentOB.
     * @param {string} key - param name.
     * @param {*} value - current value.
     * @param {*} schemaValue - the template's default value for this
     *   param, used only to pick the input type (boolean/array/single).
     * @returns {DocumentFragment|null}
     */
    function createField(stepIdx, key, value, schemaValue) {
        let type = 'single';
        if (typeof schemaValue === 'boolean') type = 'boolean';
@@ -250,6 +299,7 @@ document.addEventListener('DOMContentLoaded', () => {

    // --- 5. UI and Core Logic ---

    /** Swaps the footer to the "Save as" name-entry UI, pre-filled with the current name. */
    function showSaveAsUI() {
        document.getElementById('footer-actions-default').classList.add('d-none');
        document.getElementById('footer-actions-save-as').classList.remove('d-none');
@@ -257,11 +307,18 @@ document.addEventListener('DOMContentLoaded', () => {
        inputSaveAs.focus();
    }

    /** Swaps the footer back to the default Save/Run button row. */
    function hideSaveAsUI() {
        document.getElementById('footer-actions-default').classList.remove('d-none');
        document.getElementById('footer-actions-save-as').classList.add('d-none');
    }

    /**
     * Loads a template or block into the editor as currentOB and re-renders.
     *
     * @param {'templates'|'blocks'} type
     * @param {string} name
     */
    async function loadOB(type, name) {
        const res = await fetch(`/api/${type}/${name}`);
        const data = await res.json();
@@ -271,6 +328,13 @@ document.addEventListener('DOMContentLoaded', () => {
        renderEditor();
    }

    /**
     * Fetches (and caches) a template's default param schema, used by
     * createField() to pick each param's input type.
     *
     * @param {string} templateName
     * @returns {Promise<object>}
     */
    async function getSchema(templateName) {
        if (!schemaCache[templateName]) {
            const data = await fetch(`/api/templates/${templateName}`).then(r => r.json());
@@ -279,8 +343,12 @@ document.addEventListener('DOMContentLoaded', () => {
        return schemaCache[templateName];
    }

    // Step reordering via native HTML5 drag-and-drop; `this` is the
    // dragged/drop-target .template-block element (called via
    // addEventListener, not arrow functions, so `this` binds correctly).
    function handleDragStart(e) { dragSrcEl = this; e.dataTransfer.effectAllowed = 'move'; }
    function handleDragOver(e) { if (e.preventDefault) e.preventDefault(); return false; }
    /** Reorders currentOB by moving the dragged step to this drop target's index. */
    function handleDrop(e) {
        e.stopPropagation();
        if (dragSrcEl !== this) {
Loading