Commit 05d2505e authored by vertighel's avatar vertighel
Browse files

refactor: fits-viewer.js + viewer.html + viewer_panel — cam_id, no station



FitsViewer: costruttore senza station, URL /viewer/<cam_id>/.
fits-preview handler: {cam_id, png}, filtro this.cameras.includes(cam_id).
tile-request WS: cam_id invece di station+camera.
noctua-telemetry: rimosso guard startsWith(station).
viewer_panel macro: firma (panel_id, cam_id, has_loop, ...), data-cam-id.

Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent ce0f410e
Loading
Loading
Loading
Loading
+5 −6
Original line number Diff line number Diff line
@@ -18,18 +18,17 @@
     {% endcall %}
#}

{% macro camera_panel(panel_id, station, camera, has_loop, api_base, layout='full') %}
{% macro camera_panel(panel_id, cam_id, has_loop, api_base, layout='full') %}
<div id="viewer-panel-{{ panel_id }}"
     class="viewer-panel"
     data-station="{{ station }}"
     data-camera="{{ camera }}"
     data-cam-id="{{ cam_id }}"
     data-api-base="{{ api_base }}">

    {% set hdr_cls = 'd-flex justify-content-between align-items-center' if layout == 'full' else 'd-flex flex-column align-items-start gap-1' %}
    <div class="{{ hdr_cls }} mb-2">
        <span class="text-uppercase text-muted"
              style="font-size:.75rem; letter-spacing:.1em;">
            {{ station | title }} / {{ camera }}
            {{ cam_id }}
        </span>
        <button class="btn btn-sm btn-outline-secondary btn-refresh-{{ panel_id }}"
                title="Fetch current image">↺ Refresh</button>
@@ -101,7 +100,7 @@
            <div class="col-auto d-flex align-items-center gap-1">
                <input type="checkbox" class="ctrl-auto form-check-input"
                       id="ctrl-auto-{{ panel_id }}"
                       data-camera="{{ camera }}" checked>
                       data-cam-id="{{ cam_id }}" checked>
                <label class="text-muted small"
                       for="ctrl-auto-{{ panel_id }}">Auto&nbsp;update</label>
            </div>
@@ -183,7 +182,7 @@
                    <div class="col-auto d-flex align-items-center gap-1">
                        <input type="checkbox" class="ctrl-auto form-check-input"
                               id="ctrl-auto-{{ panel_id }}"
                               data-camera="{{ camera }}" checked>
                               data-cam-id="{{ cam_id }}" checked>
                        <label class="text-muted small"
                               for="ctrl-auto-{{ panel_id }}">Auto&nbsp;update</label>
                    </div>
+8 −8
Original line number Diff line number Diff line
@@ -12,12 +12,12 @@
{% if is_combo %}
    {% set p0 = panels[0] %}
    {% set p1 = panels[1] %}
    {% call camera_panel(p0.id, p0.station, p0.camera, p0.has_loop, api_base, layout='full') %}
        {{ camera_panel(p1.id, p1.station, p1.camera, p1.has_loop, api_base, layout='stacked') }}
    {% call camera_panel(p0.id, p0.cam_id, p0.has_loop, api_base, layout='full') %}
        {{ camera_panel(p1.id, p1.cam_id, p1.has_loop, api_base, layout='stacked') }}
    {% endcall %}
{% else %}
    {% set p = panels[0] %}
    {{ camera_panel(p.id, p.station, p.camera, p.has_loop, api_base) }}
    {{ camera_panel(p.id, p.cam_id, p.has_loop, api_base) }}
{% endif %}

{% endblock %}
@@ -27,11 +27,11 @@
<script type="module">
import { FitsViewer } from "{{ url_for('web.static', filename='js/viewer/fits-viewer.js') }}";

function initViewer(panelId, station, camera, hasLoop, apiBase) {
function initViewer(panelId, cam_id, hasLoop, apiBase) {
    const panel = document.getElementById(`viewer-panel-${panelId}`);
    if (!panel) return null;

    const viewer = new FitsViewer(station, panel, apiBase, [camera]);
    const viewer = new FitsViewer(panel, apiBase, [cam_id]);
    viewer.refresh();

    panel.querySelector(`.btn-refresh-${panelId}`)
@@ -55,7 +55,7 @@ function initViewer(panelId, station, camera, hasLoop, apiBase) {
        }

        async function loopPost(action, extra = {}) {
            return fetch(`${apiBase}/viewer/${station}/${camera}/loop`, {
            return fetch(`${apiBase}/viewer/${cam_id}/loop`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ action, ...extra }),
@@ -73,7 +73,7 @@ function initViewer(panelId, station, camera, hasLoop, apiBase) {
            setLoopUI(false);
        });

        fetch(`${apiBase}/viewer/${station}/${camera}/loop`)
        fetch(`${apiBase}/viewer/${cam_id}/loop`)
            .then(r => r.json())
            .then(d => setLoopUI(d.running))
            .catch(() => {});
@@ -86,7 +86,7 @@ const panels = {{ panels | tojson }};
const apiBase = '{{ api_base }}';

for (const p of panels) {
    initViewer(p.id, p.station, p.camera, p.has_loop, apiBase);
    initViewer(p.id, p.cam_id, p.has_loop, apiBase);
}
</script>
{% endblock %}
+22 −25
Original line number Diff line number Diff line
@@ -79,12 +79,11 @@ export class FitsViewer {
     * apiBase : string, optional
     *     URL prefix for all REST calls.  Default is '/api'.
     * cameras : string[], optional
     *     Ordered list of camera IDs from viewer.ini.
     *     Ordered list of camera IDs from cameras.ini.
     *     The first entry becomes the primary (full-canvas) camera.
     */
    constructor(station, root, apiBase = '/api', cameras = ['scicam']) {
    constructor(root, apiBase = '/api', cameras = ['scicam1']) {

        this.station = station;
        this.root    = root;
        this.api     = apiBase;
        this.cameras = cameras;
@@ -392,7 +391,7 @@ export class FitsViewer {
        const cam   = state.cam;

        try {
            const info = await this._getJson(`/viewer/${this.station}/${cam}/info`);
            const info = await this._getJson(`/viewer/${cam}/info`);
            state.width  = info.shape[1];
            state.height = info.shape[0];
            state.dtype  = info.dtype;
@@ -442,7 +441,7 @@ export class FitsViewer {
        const state  = this._primary;
        const canvas = this._el.canvasMain;
        const url    = this._url(
            `/viewer/${this.station}/${state.cam}/png`,
            `/viewer/${state.cam}/png`,
            { vmin: state.vmin, vmax: state.vmax }
        );

@@ -472,7 +471,7 @@ export class FitsViewer {
        const r      = Math.max(state.width, state.height);

        const url = this._url(
            `/viewer/${this.station}/${state.cam}/tile`,
            `/viewer/${state.cam}/tile`,
            { cx, cy, r }
        );

@@ -505,7 +504,7 @@ export class FitsViewer {
    async _refreshPanoramic() {
        const state = this._primary;
        const url   = this._url(
            `/viewer/${this.station}/${state.cam}/panoramic`,
            `/viewer/${state.cam}/panoramic`,
            { vmin: state.vmin, vmax: state.vmax, max_px: THUMB_PX }
        );

@@ -540,7 +539,7 @@ export class FitsViewer {
        if (!canvas) return;

        try {
            const info = await this._getJson(`/viewer/${this.station}/${cam}/info`);
            const info = await this._getJson(`/viewer/${cam}/info`);
            if (state.autoUpdate || state.vmin === state.vmax) {
                state.vmin = info.vmin_auto;
                state.vmax = info.vmax_auto;
@@ -552,7 +551,7 @@ export class FitsViewer {
        }

        const url = this._url(
            `/viewer/${this.station}/${cam}/png`,
            `/viewer/${cam}/png`,
            { vmin: state.vmin, vmax: state.vmax }
        );

@@ -591,18 +590,18 @@ export class FitsViewer {

        // ── fits-preview: mtime-triggered full preview broadcast ─────────────
        document.addEventListener('fits-preview', async (e) => {
            const { station, camera, png } = e.detail ?? {};
            if (station !== this.station) return;
            const { cam_id, png } = e.detail ?? {};
            if (!cam_id || !this.cameras.includes(cam_id)) return;

            if (camera === this._primaryCam) {
            if (cam_id === this._primaryCam) {
                if (!this._primary.autoUpdate) return;
                await this._paintBase64(png, this._el.canvasMain);
                this._redrawOverlay();
                await this._paintBase64(png, this._el.canvasPano);
                this._drawPanoViewport();
            } else if (this._secondary[camera]) {
                if (!this._secondary[camera].autoUpdate) return;
                const canvas = this.root.querySelector(`.cv-sec-${camera}`);
            } else if (this._secondary[cam_id]) {
                if (!this._secondary[cam_id].autoUpdate) return;
                const canvas = this.root.querySelector(`.cv-sec-${cam_id}`);
                if (canvas) await this._paintBase64(png, canvas);
            }
        });
@@ -624,7 +623,6 @@ export class FitsViewer {
        document.addEventListener('noctua-telemetry', async (e) => {
            const { name } = e.detail;
            const subsys   = name.replace('all-', '');
            if (!subsys.startsWith(this.station)) return;

            for (const cam of this.cameras) {
                if (subsys.includes(cam)) {
@@ -677,8 +675,7 @@ export class FitsViewer {
            // Fast path: fire-and-forget via the existing WebSocket connection.
            window.noctuaSocket.sendMsg({
                action: 'tile-request',
                station: this.station,
                camera:  this._primaryCam,
                cam_id: this._primaryCam,
                cx:     imageX,
                cy:     imageY,
                rx:     halfBox,
@@ -706,7 +703,7 @@ export class FitsViewer {
     * Promise<void>
     */
    async _fetchExploreTileRest(imageX, imageY, halfBox) {
        const url = `${this.api}/viewer/${this.station}/${this._primaryCam}/tile`
        const url = `${this.api}/viewer/${this._primaryCam}/tile`
                  + `?cx=${imageX}&cy=${imageY}&rx=${halfBox}&ry=${halfBox}`;
        try {
            const { data, w, h } = await this._fetchTileRest(url);