Spaces:
Paused
Paused
Download src/dashboard/static/operator_console.js from ThomasHeisig/Brain-5D-Space: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/src/dashboard/static/operator_console.js
- Command line
-
hf download hf://spaces/ThomasHeisig/Brain-5D-Space/src/dashboard/static/operator_console.js
-
curl -L -o operator_console.js https://huggingface.co/spaces/ThomasHeisig/Brain-5D-Space/resolve/main/src/dashboard/static/operator_console.js
13.6 kB
| /** | |
| * Brain-5D Operator Console — ES Module | |
| * | |
| * This module is a pure ES module. It does NOT self-initialize and does NOT | |
| * register any DOMContentLoaded listeners. The sole lifecycle owner is | |
| * `app.js`, which imports and instantiates `OperatorConsole` exactly once when | |
| * the Console tab is first activated. | |
| * | |
| * Canonical command contract (unified with ControlPanel): | |
| * POST /api/control { "command": "run_ticks", "ticks": 100 } | |
| * | |
| * No CommonJS fallbacks. No `module.exports`. No global side effects on import. | |
| * | |
| * @version 2.1.0 | |
| * @license MIT | |
| */ | |
| ; | |
| import { readJson } from "./api-client.js"; | |
| import { StructuralProposalPanel } from './structural-proposal-panel.js'; | |
| // ============================================================================ | |
| // DOM Helpers | |
| // ============================================================================ | |
| /** | |
| * Get element by ID with type safety. | |
| * @param {string} id - Element ID | |
| * @returns {HTMLElement | null} | |
| */ | |
| function byId(id) { | |
| return document.getElementById(id); | |
| } | |
| /** | |
| * Get element by ID or throw. | |
| * @param {string} id - Element ID | |
| * @param {string} context - Context for error message | |
| * @returns {HTMLElement} | |
| * @throws {Error} If element not found | |
| */ | |
| function requireElement(id, context = 'console') { | |
| const el = byId(id); | |
| if (!el) { | |
| throw new Error(`Required element "#${id}" not found in ${context}`); | |
| } | |
| return el; | |
| } | |
| /** | |
| * Set text content safely. | |
| * @param {string} id - Element ID | |
| * @param {string} text - Text to set | |
| */ | |
| function setText(id, text) { | |
| const el = byId(id); | |
| if (el) el.textContent = String(text); | |
| } | |
| /** | |
| * Set HTML content safely. | |
| * @param {string} id - Element ID | |
| * @param {string} html - HTML content | |
| */ | |
| function setHTML(id, html) { | |
| const el = byId(id); | |
| if (el) el.innerHTML = html; | |
| } | |
| // ============================================================================ | |
| // Utilities | |
| // ============================================================================ | |
| /** | |
| * Format a timestamp. | |
| * @param {Date} date - Date object | |
| * @returns {string} Formatted time | |
| */ | |
| function formatTime(date = new Date()) { | |
| return date.toLocaleTimeString('en-US', { hour12: false }); | |
| } | |
| /** | |
| * Escape HTML to prevent XSS. | |
| * @param {string} str - String to escape | |
| * @returns {string} Escaped string | |
| */ | |
| function escapeHtml(str) { | |
| if (!str) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| /** | |
| * Debounce a function. | |
| * @param {Function} fn - Function to debounce | |
| * @param {number} delay - Delay in milliseconds | |
| * @returns {Function} Debounced function | |
| */ | |
| function debounce(fn, delay) { | |
| let timer = null; | |
| return function (...args) { | |
| clearTimeout(timer); | |
| timer = setTimeout(() => fn.apply(this, args), delay); | |
| }; | |
| } | |
| // ============================================================================ | |
| // API Client | |
| // ============================================================================ | |
| export class OperatorAPI { | |
| /** | |
| * Fetch JSON from the dashboard API. | |
| * @param {string} url - API endpoint | |
| * @param {object} options - Fetch options | |
| * @returns {Promise<object>} JSON response | |
| * @throws {Error} On HTTP error or invalid response | |
| */ | |
| static async fetchJSON(url, options = {}) { | |
| const response = await fetch(url, { | |
| ...options, | |
| headers: { | |
| 'Cache-Control': 'no-store', | |
| ...(options.headers || {}), | |
| }, | |
| }); | |
| let data; | |
| try { | |
| data = await readJson(response); | |
| } catch { | |
| throw new Error(`Invalid JSON response from ${url}`); | |
| } | |
| if (!response.ok) { | |
| const message = data.error || data.message || `HTTP ${response.status}`; | |
| throw new Error(message); | |
| } | |
| return data; | |
| } | |
| /** | |
| * Send a control command. | |
| * @param {string} command - Command name | |
| * @param {object} params - Command parameters | |
| * @returns {Promise<object>} Command result | |
| */ | |
| static async sendCommand(command, params = {}) { | |
| const body = { command, ...params }; | |
| return this.fetchJSON('/api/control', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| } | |
| /** | |
| * Get system status. | |
| * @returns {Promise<object>} Status data | |
| */ | |
| static async getStatus() { | |
| return this.fetchJSON('/api/status'); | |
| } | |
| /** | |
| * Get structural proposals. | |
| * @returns {Promise<object>} Proposals data | |
| */ | |
| static async getProposals() { | |
| return this.fetchJSON('/api/structural/proposals'); | |
| } | |
| /** | |
| * Approve a structural proposal. | |
| * @param {string} proposalId - Proposal ID | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async approveProposal(proposalId) { | |
| return this.fetchJSON('/api/structural/approve', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ proposal_id: proposalId }), | |
| }); | |
| } | |
| /** | |
| * Reject a structural proposal. | |
| * @param {string} proposalId - Proposal ID | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async rejectProposal(proposalId) { | |
| return this.fetchJSON('/api/structural/reject', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ proposal_id: proposalId }), | |
| }); | |
| } | |
| /** | |
| * Undo the last structural change. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async undoStructural() { | |
| return this.fetchJSON('/api/structural/undo', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| } | |
| /** | |
| * Set auto-approval mode. | |
| * @param {boolean} enabled - Whether to enable | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async setAutoApproval(enabled) { | |
| return this.fetchJSON('/api/structural/auto-approval', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ enabled }), | |
| }); | |
| } | |
| /** | |
| * Run ticks. | |
| * @param {number} count - Number of ticks | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async runTicks(count) { | |
| return this.sendCommand('run_ticks', { ticks: count }); | |
| } | |
| /** | |
| * Single step. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async step() { | |
| return this.sendCommand('step'); | |
| } | |
| /** | |
| * Start the runtime. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async start() { | |
| return this.sendCommand('start'); | |
| } | |
| /** | |
| * Pause the runtime. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async pause() { | |
| return this.sendCommand('pause'); | |
| } | |
| /** | |
| * Resume the runtime. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async resume() { | |
| return this.sendCommand('resume'); | |
| } | |
| /** | |
| * Stop the runtime. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async stop() { | |
| return this.sendCommand('stop'); | |
| } | |
| /** | |
| * Request a snapshot. | |
| * @returns {Promise<object>} Result | |
| */ | |
| static async snapshot() { | |
| return this.sendCommand('snapshot'); | |
| } | |
| } | |
| // ============================================================================ | |
| // Console Logger (delegates to shared console-log.js) | |
| // ============================================================================ | |
| export class ConsoleLogger { | |
| constructor(containerId) { | |
| this.container = byId(containerId); | |
| this.entries = []; | |
| this.maxEntries = 1000; | |
| this.sharedLog = null; | |
| } | |
| /** | |
| * Bind to the shared console log singleton. | |
| */ | |
| async bindSharedLog() { | |
| if (this.sharedLog) return; | |
| try { | |
| const { consoleLog } = await import('./console-log.js'); | |
| this.sharedLog = consoleLog; | |
| } catch { | |
| // Shared log unavailable; fall back to local rendering. | |
| } | |
| } | |
| /** | |
| * Log a message to the console. | |
| * @param {string} message - Message to log | |
| * @param {string} type - Log type ('info', 'success', 'error', 'warning') | |
| */ | |
| log(message, type = 'info') { | |
| const entry = { | |
| timestamp: new Date(), | |
| message: String(message), | |
| type: type, | |
| }; | |
| this.entries.push(entry); | |
| if (this.entries.length > this.maxEntries) { | |
| this.entries.shift(); | |
| } | |
| if (this.sharedLog) { | |
| this.sharedLog.log(entry.message, entry.type); | |
| } else { | |
| this.render(entry); | |
| } | |
| } | |
| /** | |
| * Render a single log entry. | |
| * @param {object} entry - Log entry | |
| */ | |
| render(entry) { | |
| if (!this.container) return; | |
| const time = formatTime(entry.timestamp); | |
| const classes = `log-entry log-${entry.type}`; | |
| const div = document.createElement('div'); | |
| div.className = classes; | |
| div.innerHTML = `<span class="log-time">[${time}]</span> ${escapeHtml(entry.message)}`; | |
| this.container.appendChild(div); | |
| // Auto-scroll | |
| this.container.scrollTop = this.container.scrollHeight; | |
| // Limit DOM entries | |
| while (this.container.children.length > this.maxEntries) { | |
| this.container.removeChild(this.container.firstChild); | |
| } | |
| } | |
| /** | |
| * Clear the console. | |
| */ | |
| clear() { | |
| if (this.container) { | |
| this.container.innerHTML = ''; | |
| } | |
| this.entries = []; | |
| } | |
| /** | |
| * Get all log entries. | |
| * @returns {Array} Log entries | |
| */ | |
| getEntries() { | |
| return [...this.entries]; | |
| } | |
| } | |
| // ============================================================================ | |
| // Operator Console | |
| // ============================================================================ | |
| export class OperatorConsole { | |
| constructor() { | |
| this.logger = null; | |
| this.pollingInterval = null; | |
| this.pollingRate = 1000; | |
| this.status = null; | |
| this.proposalPanel = null; | |
| // Bind methods | |
| this.handleCommand = this.handleCommand.bind(this); | |
| this.refreshStatus = this.refreshStatus.bind(this); | |
| this.init(); | |
| } | |
| /** | |
| * Initialize the console. | |
| */ | |
| init() { | |
| // Initialize logger and bind to shared console log | |
| this.logger = new ConsoleLogger('console-output'); | |
| this.logger.bindSharedLog().then(() => { | |
| this.logger.log('🧠 Brain-5D Operator Console initialized', 'info'); | |
| this.logger.log(`📡 API endpoint: /api/control`, 'info'); | |
| this.proposalPanel = new StructuralProposalPanel({ | |
| api: OperatorAPI, | |
| logger: this.logger, | |
| onChanged: this.refreshStatus, | |
| }); | |
| this.proposalPanel.load(); | |
| }); | |
| // Bind event listeners | |
| this.bindEvents(); | |
| this.bindKeyboardShortcuts(); | |
| // Initial load | |
| this.refreshStatus(); | |
| // Start polling | |
| this.startPolling(); | |
| } | |
| /** | |
| * Bind DOM event listeners. | |
| */ | |
| bindEvents() { | |
| // Runtime controls and structural proposals are owned by their panels. | |
| // Clear console | |
| const clearBtn = byId('b5d-clear-console'); | |
| if (clearBtn) { | |
| clearBtn.addEventListener('click', () => this.logger.clear()); | |
| } | |
| } | |
| /** | |
| * Bind keyboard shortcuts. | |
| */ | |
| bindKeyboardShortcuts() { | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl+L = Clear console (if focus is on console) | |
| if (e.ctrlKey && e.key === 'l' && document.activeElement?.id === 'console-output') { | |
| e.preventDefault(); | |
| this.logger.clear(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Handle a command. | |
| * @deprecated Runtime commands are now owned by ControlPanel. | |
| */ | |
| async handleCommand(action, params = {}) { | |
| this.logger.log(`ℹ️ Runtime commands moved to Runtime Control panel (${action})`, 'info'); | |
| } | |
| /** | |
| * Handle undo structural change. | |
| * @deprecated Structural undo is now owned by ControlPanel. | |
| */ | |
| async handleUndo() { | |
| this.logger.log('ℹ️ Structural undo moved to Runtime Control panel', 'info'); | |
| } | |
| /** | |
| * Refresh system status. | |
| */ | |
| async refreshStatus() { | |
| try { | |
| const data = await OperatorAPI.getStatus(); | |
| this.status = data; | |
| this.renderStatus(data); | |
| } catch (error) { | |
| this.logger.log(`⚠️ Failed to refresh status: ${error.message}`, 'warning'); | |
| } | |
| } | |
| /** | |
| * Render system status. | |
| * @param {object} data - Status data | |
| */ | |
| renderStatus(data) { | |
| // Update status badge | |
| const badge = byId('b5d-state-badge'); | |
| if (badge) { | |
| const state = data.state || 'idle'; | |
| badge.textContent = state; | |
| badge.className = `status-badge status-${state}`; | |
| } | |
| // Update tick count | |
| const tickEl = byId('b5d-tick-display'); | |
| if (tickEl) { | |
| tickEl.textContent = String(data.tick || 0); | |
| } | |
| // Update metrics | |
| const metrics = byId('b5d-metrics'); | |
| if (metrics && data.system) { | |
| const s = data.system; | |
| metrics.innerHTML = ` | |
| <span>🧠 ${(s.neurons || 0).toLocaleString()}</span> | |
| <span>🔗 ${(s.synapses || 0).toLocaleString()}</span> | |
| <span>⚡ ${(s.spikes_total || 0).toLocaleString()}</span> | |
| `; | |
| } | |
| } | |
| /** | |
| * Start polling for status updates. | |
| */ | |
| startPolling() { | |
| if (this.pollingInterval) { | |
| clearInterval(this.pollingInterval); | |
| } | |
| this.pollingInterval = setInterval(() => { | |
| this.refreshStatus().catch(() => {}); | |
| }, this.pollingRate); | |
| } | |
| /** | |
| * Stop polling. | |
| */ | |
| stopPolling() { | |
| if (this.pollingInterval) { | |
| clearInterval(this.pollingInterval); | |
| this.pollingInterval = null; | |
| } | |
| } | |
| /** | |
| * Set polling rate. | |
| * @param {number} ms - Milliseconds | |
| */ | |
| setPollingRate(ms) { | |
| if (ms < 100) ms = 100; | |
| this.pollingRate = ms; | |
| if (this.pollingInterval) { | |
| this.startPolling(); | |
| } | |
| } | |
| /** | |
| * Destroy the console instance. | |
| */ | |
| destroy() { | |
| this.stopPolling(); | |
| this.logger.log('🛑 Operator Console shutting down', 'info'); | |
| } | |
| } | |