Loading noctua/api/deps.py +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: Loading @@ -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 Loading noctua/config/api.ini +6 −4 Original line number Diff line number Diff line Loading @@ -120,6 +120,7 @@ depends-on = /telescope/power [/telescope/lamp] resource = State device = lamp depends-on = /dome/connection [/telescope/cover] resource = Cover Loading @@ -129,6 +130,7 @@ depends-on = /telescope/clock [/telescope/cover/movement] resource = CoverMovement device = tel depends-on = /telescope/power [/telescope/coordinates] resource = Coordinates Loading Loading @@ -311,6 +313,10 @@ depends-on = /camera/power # webcam ############## [/webcam/stream] resource = VideoStream device = ipcam [/webcam/snapshot] resource = Snapshot device = ipcam Loading @@ -319,10 +325,6 @@ device = ipcam resource = Pointing device = ipcam [/webcam/stream] resource = VideoStream device = ipcam # ############## # # environment # ############## Loading noctua/utils/check.py +23 −9 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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: Loading noctua/web/pages/init.html +4 −1 Original line number Diff line number Diff line Loading @@ -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 }) }} Loading noctua/web/static/js/status-view.js +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'); Loading @@ -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', () => { Loading @@ -22,6 +23,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } // Register active layout visibility rules dynamically if (globalRawCheckbox) { globalRawCheckbox.addEventListener('change', () => { Loading @@ -35,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } // Helper to safely strip volatile properties for change comparison function getComparableValue(val) { if (!val) return null; Loading @@ -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; Loading @@ -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); Loading @@ -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'); Loading @@ -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}`); Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading @@ -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 Loading
noctua/api/deps.py +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: Loading @@ -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 Loading
noctua/config/api.ini +6 −4 Original line number Diff line number Diff line Loading @@ -120,6 +120,7 @@ depends-on = /telescope/power [/telescope/lamp] resource = State device = lamp depends-on = /dome/connection [/telescope/cover] resource = Cover Loading @@ -129,6 +130,7 @@ depends-on = /telescope/clock [/telescope/cover/movement] resource = CoverMovement device = tel depends-on = /telescope/power [/telescope/coordinates] resource = Coordinates Loading Loading @@ -311,6 +313,10 @@ depends-on = /camera/power # webcam ############## [/webcam/stream] resource = VideoStream device = ipcam [/webcam/snapshot] resource = Snapshot device = ipcam Loading @@ -319,10 +325,6 @@ device = ipcam resource = Pointing device = ipcam [/webcam/stream] resource = VideoStream device = ipcam # ############## # # environment # ############## Loading
noctua/utils/check.py +23 −9 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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: Loading
noctua/web/pages/init.html +4 −1 Original line number Diff line number Diff line Loading @@ -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 }) }} Loading
noctua/web/static/js/status-view.js +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'); Loading @@ -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', () => { Loading @@ -22,6 +23,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } // Register active layout visibility rules dynamically if (globalRawCheckbox) { globalRawCheckbox.addEventListener('change', () => { Loading @@ -35,6 +37,7 @@ document.addEventListener('DOMContentLoaded', () => { }); } // Helper to safely strip volatile properties for change comparison function getComparableValue(val) { if (!val) return null; Loading @@ -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; Loading @@ -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); Loading @@ -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'); Loading @@ -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}`); Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading @@ -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