Commit 6f20521c authored by vertighel's avatar vertighel
Browse files

Tree dependency

parent ee7fa764
Loading
Loading
Loading
Loading
Loading
+27 −50
Original line number Diff line number Diff line
# System modules
import time

# Third-party modules
# deps.py
# Dependency Manager providing topological priorities and REST route verification.

# Custom modules
import time
from noctua.api.basecontext import ends, resource_registry


class DependencyCache:
    """
    Manages dependency verification with a short-lived internal cache.
    Manages dependency verification with a simple time-based cache for single calls,
    and pre-calculates topological priority levels for all endpoints.
    """

    def __init__(self, ttl=0.8):
        """
        Initialize the dependency cache.

        Parameters
        ----------
        ttl : float
            Time-to-live for cache entries in seconds.
        """
        self._cache = {}
        self._ttl = ttl
        self.priorities = {}


    async def get_status(self, path, force=False):
    def calculate_priorities(self):
        """
        Check if a resource is available based on its dependencies.

        Parameters
        ----------
        path : str
            The API path to verify.
        force : bool
            If True, bypass all checks.

        Returns
        -------
        tuple
            A tuple containing (is_available, error_message).
        Calculate the hierarchical priority level (0 = root, 1 = child, etc.)
        for all active resources based on depends-on declarations.
        """
        def get_level(path):
            parent = ends.get(path, "depends-on", fallback=None)
            if not parent or parent not in resource_registry:
                return 0
            return get_level(parent) + 1

        self.priorities = {path: get_level(path) for path in resource_registry.keys()}


    async def get_status(self, path, force=False):
        """
        Verify if an endpoint is allowed to execute based on its parents.
        """
        if force:
            return True, None

        now = time.time()

        if path in self._cache:
            status, timestamp = self._cache[path]
            if (now - timestamp) < self._ttl:
@@ -59,41 +50,27 @@ class DependencyCache:


    async def _perform_check(self, path, force):
        """
        Logic for recursive dependency resolution.

        Parameters
        ----------
        path : str
            The API path.
        force : bool
            The force flag.

        Returns
        -------
        tuple
            The check result.
        """

        dependency = ends.get(path, "depends-on", fallback=None)

        if not dependency:
            return True, None

        parent_resource = resource_registry.get(dependency)

        if not parent_resource:
            return False, f"Missing resource: {dependency}"

        # Recursive call to resolve the parent's own dependencies
        # Recursive check for single REST requests
        parent_ok, parent_err = await self.get_status(dependency, force)
        if not parent_ok:
            return False, parent_err

        # Check parent actual state
        data, status_code = await parent_resource.get()
        response = await parent_resource.get()
        data = response[0] if isinstance(response, tuple) else response
        
        raw_val = data.get("raw")

        if status_code != 200 or not data.get("raw") or data.get("error"):
        if raw_val is False or raw_val is None or data.get("error"):
            return False, f"Link failure: {dependency}"

        return True, None
+6 −4
Original line number Diff line number Diff line
@@ -120,6 +120,7 @@ depends-on = /telescope/power
[/telescope/lamp]
resource = State
device = lamp
depends-on = /dome/connection

[/telescope/cover]
resource = Cover
@@ -129,6 +130,7 @@ depends-on = /telescope/clock
[/telescope/cover/movement]
resource = CoverMovement
device = tel
depends-on = /telescope/power

[/telescope/coordinates]
resource = Coordinates
@@ -311,6 +313,10 @@ depends-on = /camera/power
# webcam
##############

[/webcam/stream]
resource = VideoStream
device = ipcam

[/webcam/snapshot]
resource = Snapshot
device = ipcam
@@ -319,10 +325,6 @@ device = ipcam
resource = Pointing
device = ipcam

[/webcam/stream]
resource = VideoStream
device = ipcam

# ##############
# # environment
# ##############
+23 −9
Original line number Diff line number Diff line
@@ -108,12 +108,19 @@ def request_errors(func):

            return

        # except (requests.exceptions.Timeout, requests.exceptions.ConnectTimeout) as e:
        #     msg = f"{name}: Timeout or Connection timeout!"
        #     log.error(msg)
        #     log.error(e)
        #     this.error.append(msg)
        #     this.error.append(str(e))
        #     return []

        except (requests.exceptions.Timeout, requests.exceptions.ConnectTimeout) as e:
            msg = f"{name}: Timeout or Connection timeout!"
            log.error(msg)
            log.error(e)
            msg = f"{name}: Timeout!"
            # DOWNGRADED TO DEBUG: Do not flood the console on continuous polling
            log.debug(msg)
            this.error.append(msg)
            this.error.append(str(e))
            return []
        
        except requests.exceptions.TooManyRedirects as e:
@@ -132,12 +139,19 @@ def request_errors(func):
            this.error.append(str(e))
            return

        # except requests.exceptions.ConnectionError as e:
        #     msg = f"{name}: Connection error!"
        #     log.error(msg)
        #     log.error(e)
        #     this.error.append(msg)
        #     this.error.append(str(e))
        #     return []

        except requests.exceptions.ConnectionError as e:
            msg = f"{name}: Connection error!"
            log.error(msg)
            log.error(e)
            msg = f"{name}: Connection refused or offline!"
            # DOWNGRADED TO DEBUG: Silent fail for offline Layer 1 devices during polling
            log.debug(msg)
            this.error.append(msg)
            this.error.append(str(e))
            return []

        except AttributeError as e:
+4 −1
Original line number Diff line number Diff line
@@ -146,7 +146,10 @@
              w.widget_toggle({
                "label": "Is Cam On?",
                "info": "/camera/power",
                "buttons": [{"label": "Power", "endpoint": "/camera/power"}],
                "buttons": [
                    {"label": "On", "endpoint": "/camera/power", "val": true, "method": "PUT"},
                    {"label": "Off", "endpoint": "/camera/power", "val": false, "method": "PUT"}
                ],
                "extra_flags": True
              })
            }}
+34 −22
Original line number Diff line number Diff line
// status-view.js
// Compiles blueprint telemetry cards once, sets up declarative data-status mappings, and handles local cell-level pulsing.
// Renders subsystem tables dynamically, splitting 2-element arrays into two lines and objects into structured sub-tables.

document.addEventListener('DOMContentLoaded', () => {
    const tableBlueprint = document.getElementById('table-blueprint');
@@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
    // Central state cache to remember previous telemetry values for change detection
    const previousState = {};


    // Register active global force listener to communicate change over WebSocket
    if (globalForceCheckbox) {
        globalForceCheckbox.addEventListener('change', () => {
@@ -22,6 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
        });
    }


    // Register active layout visibility rules dynamically
    if (globalRawCheckbox) {
        globalRawCheckbox.addEventListener('change', () => {
@@ -35,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => {
        });
    }


    // Helper to safely strip volatile properties for change comparison
    function getComparableValue(val) {
        if (!val) return null;
@@ -48,6 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
        return val;
    }


    document.addEventListener('noctua-telemetry', (event) => {
        const msg = event.detail;
        if (!msg || !msg.name.startsWith('all-')) return;
@@ -68,18 +72,18 @@ document.addEventListener('DOMContentLoaded', () => {

        const showRaw = globalRawCheckbox ? globalRawCheckbox.checked : false;

        // Initialize state cache for the subsystem if missing
        if (!previousState[subsystem]) {
            previousState[subsystem] = {};
        }

        // Render raw JSON display
        if (dataPre) {
            dataPre.textContent = JSON.stringify(data, null, 4);
            dataPre.classList.toggle('d-none', !showRaw);
        }

        // Initialize state cache for the subsystem if missing
        if (!previousState[subsystem]) {
            previousState[subsystem] = {};
        }

        // 1. One-time table DOM assembly (only runs on first websocket message for this subsystem)
        // 1. One-time table DOM assembly (builds the structure once)
        if (prettyDiv && prettyDiv.children.length === 0 && tableBlueprint && rowBlueprint) {
            prettyDiv.classList.toggle('d-none', showRaw);

@@ -100,8 +104,8 @@ document.addEventListener('DOMContentLoaded', () => {
                    responseValue = val;
                }

                // If nested response, build nested subtable with individual cell hooks
                if (responseValue && typeof responseValue === 'object') {
                // If response is a structured object (excluding arrays), build a nested sub-table
                if (responseValue && typeof responseValue === 'object' && !Array.isArray(responseValue)) {
                    if (subtableBlueprint && subrowBlueprint) {
                        const subtableClone = subtableBlueprint.content.cloneNode(true);
                        const subTbody = subtableClone.querySelector('.sub-rows');
@@ -110,7 +114,7 @@ document.addEventListener('DOMContentLoaded', () => {
                            const subrowClone = subrowBlueprint.content.cloneNode(true);
                            subrowClone.querySelector('.key-cell').textContent = k.replace('_', ' ');
                            
                            // Map deep declarative data-status
                            // Bind nested status attribute
                            const valCell = subrowClone.querySelector('.val-cell');
                            valCell.setAttribute('data-status', `${subsystem}-${deviceKey}-${k}`);
                            
@@ -119,20 +123,18 @@ document.addEventListener('DOMContentLoaded', () => {
                        statusTd.appendChild(subtableClone);
                    }
                } else {
                    // Map flat declarative data-status
                    // Standard values or arrays are bound flatly and rendered inside the cell
                    statusTd.setAttribute('data-status', `${subsystem}-${deviceKey}`);
                }

                // Map error badge declarative status
                errTd.innerHTML = `<span class="badge" data-status="${subsystem}-${deviceKey}-error">N/A</span>`;

                tbody.appendChild(rowClone);
            });

            prettyDiv.appendChild(tableClone);
        }

        // 2. High-performance, in-place cell updates using precise query selectors
        // 2. High-performance, in-place cell updates
        const elements = prettyDiv.querySelectorAll('[data-status]');
        elements.forEach(el => {
            const statusKey = el.dataset.status;
@@ -146,7 +148,7 @@ document.addEventListener('DOMContentLoaded', () => {

            let finalValue = 'N/A';

            // Special case: update error badge cell in place
            // Special case: update error badge cell
            if (subProperty === 'error') {
                const errors = endpointData.error;
                const hasErrors = errors && errors.length > 0;
@@ -187,9 +189,18 @@ document.addEventListener('DOMContentLoaded', () => {
                finalValue = 'N/A';
            }

            // Dynamic layout generation: if value is a 2-element array, split on two lines
            let displayHTML = '';
            const isArray2 = Array.isArray(finalValue) && finalValue.length === 2;

            if (isArray2) {
                displayHTML = `<div class="small fw-bold">${finalValue[0]}</div><div class="small fw-bold">${finalValue[1]}</div>`;
            } else {
                const displayValue = typeof finalValue === 'object' ? JSON.stringify(finalValue) : finalValue;
                displayHTML = displayValue.toString();
            }

            // Compare specific displayed value only (ignoring changes in volatile metadata like timestamps)
            // Compare specific rendered HTML to trigger precise cell pulse
            const prevCompare = getComparableValue(previousState[subsystem][statusKey]);
            const currCompare = getComparableValue(endpointData);

@@ -204,14 +215,15 @@ document.addEventListener('DOMContentLoaded', () => {
                setTimeout(() => el.classList.remove('pulse-update'), 50);
            }

            el.textContent = displayValue;
            el.innerHTML = displayHTML;

            // Dynamic badge coloring in status card list
            if (el.classList.contains('badge')) {
            // Auto-color badges
            if (el.classList.contains('badge') && !isArray2) {
                const v = el.textContent.toLowerCase();
                el.className = 'badge';
                if (displayValue === 'On' || displayValue === 'Yes' || displayValue === 'Open') {
                if (v === 'on' || v === 'yes' || v === 'open' || v === 'true') {
                    el.classList.add('bg-success');
                } else if (displayValue === 'Off' || displayValue === 'No' || displayValue === 'Closed') {
                } else if (v === 'off' || v === 'no' || v === 'closed' || v === 'false') {
                    el.classList.add('bg-danger');
                } else {
                    el.classList.add('bg-secondary');
Loading