Commit 3f1d5a2d authored by vertighel's avatar vertighel
Browse files

Refactoring js

parent 3e22d753
Loading
Loading
Loading
Loading
Loading
+12 −12
Original line number Diff line number Diff line
@@ -157,21 +157,21 @@
        </div>
    </footer>

    <!-- ── Vendor scripts ─────────────────────────────────────────────────── -->
    <script type="module" src="{{ url_for('web.static', filename='js/ui-utils.js') }}"></script>
    <!-- Prima i fogli di utilità principali caricati come moduli -->
    <script type="module" src="{{ url_for('web.static', filename='js/ui-core.js') }}"></script>
    <script type="module" src="{{ url_for('web.static', filename='js/ui.js') }}"></script>
    
    <!-- Vendor esterni tradizionali non-modulari -->
    <script src="{{ url_for('web.static', filename='js/vendor/bootstrap.bundle.min.js') }}"></script>
    <script src="{{ url_for('web.static', filename='js/vendor/ansi_up.js') }}"></script>
    
    <!-- ── UI primitives (must come before actions / status scripts) ──────── -->
    <script src="{{ url_for('web.static', filename='js/ui.js') }}"></script>

    <!-- ── Noctua Web Components ──────────────────────────────── -->
    <!-- I moduli reattivi e di comunicazione -->
    <script type="module" src="{{ url_for('web.static', filename='js/status-stream.js') }}"></script>
    <script type="module" src="{{ url_for('web.static', filename='js/status-view.js') }}"></script>
    <script type="module" src="{{ url_for('web.static', filename='js/ws-client.js') }}"></script>
    
    <!-- ── Unified WebSocket client ───────────────────────────────────────── -->
    <script src="{{ url_for('web.static', filename='js/ui-styles.js') }}"></script>
    <script src="{{ url_for('web.static', filename='js/status-stream.js') }}"></script>
    <!-- Gli altri script di pagina tradizionali e web components -->
    <script src="{{ url_for('web.static', filename='js/actions.js') }}"></script>
    <script src="{{ url_for('web.static', filename='js/ws-client.js') }}"></script>
    <script type="module" src="{{ url_for('web.static', filename='js/components/NoctuaWidget.js') }}"></script>
    
    {% block scripts %}{% endblock %}
+1 −1
Original line number Diff line number Diff line
@@ -57,6 +57,6 @@
{% endblock %}

{% block scripts %}
  <script src="{{ url_for('web.static', filename='js/sequencer.js') }}"></script>
  <script type="module" src="{{ url_for('web.static', filename='js/sequencer.js') }}"></script>
{% endblock %}
    
+1 −1
Original line number Diff line number Diff line
@@ -39,5 +39,5 @@
{% endblock %}

{% block scripts %}
<script src="{{ url_for('web.static', filename='js/status-view.js') }}"></script>
<script type="module" src="{{ url_for('web.static', filename='js/status-view.js') }}"></script>
{% endblock %}
+1 −1
Original line number Diff line number Diff line
@@ -23,5 +23,5 @@
    window.SYNOPTIC_SVG_URL = "{{ url_for('web.static', filename='img/synoptic.svg') }}";
  </script>
    <!-- ── Synoptic animation  ───────────────────────────────────────── -->
  <script src="{{ url_for('web.static', filename='js/synoptic.js') }}"></script>
  <script type="module" src="{{ url_for('web.static', filename='js/synoptic.js') }}"></script>
{% endblock %}
+11 −66
Original line number Diff line number Diff line
// noctua/web/static/js/components/NoctuaWidget.js
// NoctuaWidget.js
// Dynamic Web Component for automatic UI Widget generation.

import { normalizePath } from '../ui-core.js';

// Global cache to prevent redundant API schema fetches
let apiSchemaCache = null;
@@ -6,13 +9,11 @@ let apiSchemaCache = null;
export class NoctuaWidget extends HTMLElement {
    constructor() {
        super();
        // Do not read DOM attributes here, wait for connectedCallback
        this.route = '';
        this.method = '';
    }

    async connectedCallback() {
        // Read HTML attributes now that the element is attached to the DOM
        this.route = this.getAttribute('route') || '';
        this.method = (this.getAttribute('method') || 'get').toUpperCase();

@@ -22,13 +23,9 @@ export class NoctuaWidget extends HTMLElement {
        }

        try {
            // Load the API schema if not already cached
            const schema = await this._getApiSchema();
            
            // Find the endpoint metadata
            let meta = this._findEndpointMetadata(schema);

            // Handle manual override for previews/mock devices
            const overrideAttr = this.getAttribute('expects-override');
            if (overrideAttr) {
                try {
@@ -43,17 +40,12 @@ export class NoctuaWidget extends HTMLElement {
                return;
            }

            // Determine if the resource has GET capability on the exact same endpoint
            const hasGet = this._checkGetCapability(schema);

            // Select and clone the appropriate Jinja2 HTML template
            this._render(meta, hasGet);

            // Trigger an initial GET action automatically to load the state on boot (using actions.js trigger)
            if (hasGet) {
                const refreshBtn = this.querySelector('.btn-refresh');
                if (refreshBtn) {
                    // Programmatically trigger a click event to let actions.js fetch the initial state (English comment)
                    refreshBtn.dispatchEvent(new Event('click', { bubbles: true }));
                }
            }
@@ -64,9 +56,6 @@ export class NoctuaWidget extends HTMLElement {
        }
    }

    /**
     * Fetch the centralized API schema from the backend.
     */
    async _getApiSchema() {
        if (apiSchemaCache) {
            return apiSchemaCache;
@@ -79,41 +68,20 @@ export class NoctuaWidget extends HTMLElement {
        return apiSchemaCache;
    }

    /**
     * Standardize route paths by removing leading/trailing slashes.
     */
    _normalizePath(p) {
        let path = p.replace(/^\/|\/$/g, '').toLowerCase();
        if (path.startsWith('api/')) {
            path = path.slice(4);
        }
        return path;
    }

    /**
     * Find the matching route and method configuration in the schema catalog.
     */
    _findEndpointMetadata(schema) {
        const target = this._normalizePath(this.route);
        return schema.find(item => this._normalizePath(item.endpoint) === target && item.method === this.method);
        const target = normalizePath(this.route);
        return schema.find(item => normalizePath(item.endpoint) === target && item.method === this.method);
    }

    /**
     * Verify if the endpoint supports the GET method in the registry.
     */
    _checkGetCapability(schema) {
        const target = this._normalizePath(this.route);
        return schema.some(item => this._normalizePath(item.endpoint) === target && item.method === 'GET');
        const target = normalizePath(this.route);
        return schema.some(item => normalizePath(item.endpoint) === target && item.method === 'GET');
    }

    /**
     * Choose, clone, and populate the appropriate HTML5 template.
     */
    _render(meta, hasGet) {
        const expects = meta.expects || {};
        let templateId = 'tpl-widget-readout'; // Default fallback
        let templateId = 'tpl-widget-readout'; 

        // Explicitly check for GET first to avoid empty button fallbacks
        if (this.method === 'GET') {
            templateId = 'tpl-widget-readout';
        } else if (this.method === 'DELETE' || !meta.expects) {
@@ -132,32 +100,22 @@ export class NoctuaWidget extends HTMLElement {
            return;
        }

        // Clone the template content
        const clone = template.content.cloneNode(true);
        const normalizedName = normalizePath(this.route);

        // Normalize route name (e.g., "telescope-coordinates-movement-altaz")
        const normalizedName = this.route.replace(/^\/|\/$/g, '').replace(/\//g, '-');

        // Populate and bind semantic elements
        this._populateLabelsAndUnits(clone, expects, hasGet);
        this._bindSemanticDataAttributes(clone, normalizedName, expects);

        // Clear loading indicators and append to Light DOM for easy Bootstrap and global script access
        this.innerHTML = '';
        this.appendChild(clone);
    }

    /**
     * Inject custom labels, units, and placeholders from metadata.
     */
    _populateLabelsAndUnits(clone, expects, hasGet) {
        // Set main title from route
        const titleEl = clone.querySelector('.widget-title');
        if (titleEl) {
            titleEl.textContent = this.route.split('/').pop().replace(/_/g, ' ');
        }

        // Handle disabled visual cue for write-only endpoints
        const badge = clone.querySelector('.widget-status-badge');
        const refreshBtn = clone.querySelector('.btn-refresh');

@@ -173,19 +131,16 @@ export class NoctuaWidget extends HTMLElement {
            }
        }

        // Set unit badge
        const unitEl = clone.querySelector('.widget-unit');
        if (unitEl && expects.unit) {
            unitEl.textContent = expects.unit;
        }

        // Handle specific configurations for single input (supports number and string types)
        const singleInput = clone.querySelector('.widget-input-1');
        if (singleInput && (expects.type === 'single' || expects.type === 'number' || expects.type === 'string')) {
            if (expects.placeholder) singleInput.placeholder = expects.placeholder;
        }

        // Handle specific configurations for dual inputs
        if (expects.type === 'array' && expects.count === 2) {
            const input1 = clone.querySelector('.widget-input-1');
            const input2 = clone.querySelector('.widget-input-2');
@@ -198,18 +153,13 @@ export class NoctuaWidget extends HTMLElement {
            if (label2) label2.textContent = 'Val 2';
        }

        // Handle text label inside impulse buttons (DELETE or parameterless actions)
        const actionBtn = clone.querySelector('.btn-control:not(.btn-refresh)');
        if (actionBtn && (this.method === 'DELETE' || !expects.type)) {
            actionBtn.textContent = this.method === 'DELETE' ? 'ABORT' : 'EXECUTE';
        }
    }

    /**
     * Map data-status, data-control, data-parameter and standard id attributes for actions.js compatibility.
     */
    _bindSemanticDataAttributes(clone, normalizedName, expects) {
        // Bind dynamic DOM IDs to inputs so actions.js can resolve them
        const input1 = clone.querySelector('.widget-input-1');
        const input2 = clone.querySelector('.widget-input-2');
        
@@ -225,26 +175,22 @@ export class NoctuaWidget extends HTMLElement {
            input2.setAttribute('data-parameter', normalizedName);
        }

        // Set websocket status listener
        const statusBadge = clone.querySelector('.widget-status-badge');
        if (statusBadge) {
            statusBadge.id = `badge-${normalizedName}`;
            statusBadge.setAttribute('data-status', normalizedName);
        }

        // Bind control targets compatibile with actions.js event delegation
        clone.querySelectorAll('.btn-control').forEach(btn => {
            btn.classList.add('btn-universal'); // Add btn-universal class so actions.js intercepts clicks
            btn.classList.add('btn-universal'); 
            btn.setAttribute('data-safe-id', normalizedName);
            btn.setAttribute('data-url', `/${this.route.replace(/^\/|\/$/g, '')}`);
            
            // Set dynamic method ONLY if it is not pre-defined in the HTML template (like GET on the refresh button)
            const btnMethod = btn.getAttribute('data-method');
            if (!btnMethod || btnMethod.trim() === '') {
                btn.setAttribute('data-method', this.method);
            }

            // Assign standard inputs parameter list to button dataset
            if (this.method !== 'GET') {
                if (expects.type === 'single' || expects.type === 'number' || expects.type === 'string') {
                    btn.setAttribute('data-inputs', id1);
@@ -256,5 +202,4 @@ export class NoctuaWidget extends HTMLElement {
    }
}

// Register the custom HTML element
customElements.define('noctua-widget', NoctuaWidget);
Loading