/** * Brain-5D Operator Dashboard – Main Application (Sole Lifecycle Owner) * * This module is the ONLY component that initializes dashboard sub-modules. * ControlPanel and OperatorConsole are pure ES modules that do NOT * self-initialize. app.js imports them statically and instantiates each * exactly once, when the Control & Console tab is first activated. * * Canonical command contract (unified across Control + Console): * POST /api/control { "command": "run_ticks", "ticks": 100 } * * Tab-based navigation (5 main areas): * - OVERVIEW: high-level runtime KPIs, health/errors, active profile * - NETWORK: all scientific visualizations (projection, IO-flow, populations, * raster, histogram, layer explorer, inspector, neuron/synapse tables) * - CONTROL & CONSOLE: unified operator workflow, structural live loop, * console log, proposals * - RESEARCH & DOCS: unified file manager for research artifacts and docs * - RELEASE: Alpha.5 integration gate and verification status * * Features: * - Real-time system monitoring with auto-refresh * - Heatmap visualization * - Runtime control (step, run, pause, stop, snapshot) * - Structural plasticity management (proposals, approve, reject, undo) * - Research registry browser (B5D-SEF) * - Alpha.5 Integration Gate status board * - Documentation browsing with multi-format support * - Keyboard shortcuts for all major actions * * @version 3.1.0 * @license MIT */ "use strict"; // ================================================================ // IMPORTS — static ES module imports (no dynamic fallback) // ================================================================ import { ControlPanel } from './control-panel.js'; import { OperatorConsole } from './operator_console.js'; import { initResearchBrowser, initDocumentationBrowser, refreshFileManager } from './file-viewer.js'; import { dashboardStore } from './state-store.js'; import { initHealthDrawer } from './health-drawer.js'; import { consoleLog } from './console-log.js'; import { ParameterInspector } from './parameter-inspector.js'; import { ExperimentMode } from './experiment-mode.js'; import { ExperimentWorkflowPanel } from './experiment-workflow.js'; import { renderOverviewCommandCenter, setupOverviewActions } from './overview-panel.js'; import { SettingsPanel } from './settings-panel.js'; import { initEmbodimentDetails, initEmbodimentPipelineControls, renderWorkspaceSummaries } from './workspace-panels.js'; import { initResearchChat } from './research-chat.js'; import { initBoxStates } from './box-state-controller.js'; import { renderGateBoard } from './gate-board.js'; import { pollInterval, readJson } from './api-client.js'; // ================================================================ // DOM HELPERS // ================================================================ const $ = (id) => document.getElementById(id); const $$ = (sel) => document.querySelectorAll(sel); function setText(id, value) { const el = $(id); if (el) el.textContent = String(value); } function setHTML(id, html) { const el = $(id); if (el) el.innerHTML = html; } function setClass(id, className) { const el = $(id); if (el) el.className = className; } // ================================================================ // UTILITIES // ================================================================ function formatBytes(value) { // null/undefined => unavailable, NOT fake zero if (value === null || value === undefined) return '—'; const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; let size = Number(value); let idx = 0; while (size >= 1024 && idx < units.length - 1) { size /= 1024; idx++; } return `${size.toFixed(idx === 0 ? 0 : 2)} ${units[idx]}`; } function formatNumber(value) { // null/undefined => unavailable, NOT fake zero if (value === null || value === undefined) return '—'; return Number(value).toLocaleString(); } function formatFloat(value, decimals = 3) { // null/undefined => unavailable, NOT fake zero if (value === null || value === undefined) return '—'; return Number(value).toFixed(decimals); } /** * Format an optional metric with a unit suffix. * null/undefined => "—" (unavailable), measured zero => "0.000 unit". */ function formatMetricUnit(value, unit, decimals = 3) { if (value === null || value === undefined) return '—'; return `${Number(value).toFixed(decimals)} ${unit}`; } function debounce(fn, delay) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } /** * Heatmap color mapping based on value range. */ function heatmapColor(value, min, max) { const scale = max > min ? (value - min) / (max - min) : 0; const t = Math.max(0, Math.min(1, scale)); const hue = 210 - (195 * t); const light = 16 + (46 * t); return `hsl(${hue}, 88%, ${light}%)`; } // ================================================================ // TAB SWITCHING // ================================================================ function setupTabs() { const navigation = document.querySelector('.tab-nav'); if (!navigation) return {}; // Track initialization state const initialized = { network: false, control: false, research: false, gate: false, settings: false, }; // Component instances const instances = { control: null, console: null, parameterInspector: null, experimentMode: null, experimentWorkflow: null, settings: null, }; navigation.addEventListener('click', (event) => { const btn = event.target.closest('.tab-btn'); if (!btn || !navigation.contains(btn)) return; const buttons = navigation.querySelectorAll('.tab-btn'); buttons.forEach(b => b.classList.toggle('active', b === btn)); const tabName = btn.dataset.tab; const currentTab = document.body.dataset.currentTab; if (currentTab && currentTab !== tabName) { document.body.dataset.previousTab = currentTab; } document.body.dataset.currentTab = tabName; setText('header-context', tabName === 'gate' ? 'Release' : `${tabName.charAt(0).toUpperCase()}${tabName.slice(1)}`); document.querySelectorAll('.tab-content').forEach((el) => { const active = el.id === `tab-${tabName}`; el.classList.toggle('active', active); el.hidden = !active; }); // Lazy initialize components when their tab becomes visible if (tabName === 'control' && !initialized.control) { instances.control = initControlPanel(); instances.console = initOperatorConsole(); if (sharedExperimentMode) { instances.experimentMode = sharedExperimentMode; } else { instances.experimentMode = new ExperimentMode(); instances.experimentMode.refresh(); } initialized.control = true; } if (tabName === 'research' && !initialized.research) { initResearchBrowser(); initDocumentationBrowser(); instances.experimentWorkflow = new ExperimentWorkflowPanel({ onCompleted: () => refreshFileManager(), }); instances.experimentWorkflow.refresh(); initialized.research = true; } if (tabName === 'gate' && !initialized.gate) { initGateBoard(); initialized.gate = true; } if (tabName === 'settings' && !initialized.settings) { instances.parameterInspector = initParameterInspector(); instances.settings = new SettingsPanel(instances.parameterInspector); initialized.settings = true; } }); // Activate the first tab by default (Dashboard) const firstTab = navigation.querySelector('.tab-btn'); if (firstTab) { firstTab.click(); } initResearchChat(); return instances; } function setupWorkspaceViews() { document.querySelectorAll('[data-workspace-views]').forEach((navigation) => { const workspace = navigation.dataset.workspaceViews; navigation.addEventListener('click', (event) => { const button = event.target.closest('[data-workspace-view]'); if (!button) return; const view = button.dataset.workspaceView; navigation.querySelectorAll('[data-workspace-view]').forEach((item) => { item.classList.toggle('active', item === button); }); document.querySelectorAll(`[data-${workspace}-view][data-workspace-panel]`).forEach((panel) => { panel.hidden = panel.dataset[`${workspace}View`] !== view; }); }); }); } function setupResearchLanes() { const lanes = $('research-lanes'); lanes?.addEventListener('click', (event) => { const button = event.target.closest('[data-research-source]'); if (!button) return; lanes.querySelectorAll('[data-research-source]').forEach((item) => { item.classList.toggle('active', item === button); }); const source = button.dataset.researchSource; const sourceButton = document.querySelector(`.fm-source-btn[data-source="${source}"]`); const sourceChanged = sourceButton && !sourceButton.classList.contains('active'); if (sourceChanged) sourceButton.click(); const search = $('fm-search'); if (!search) return; const runLane = () => { search.value = button.dataset.researchQuery || ''; $('fm-search-btn')?.click(); }; if (sourceChanged) { setTimeout(runLane, 150); } else { runLane(); } }); } // ================================================================ // DASHBOARD – Status & Heatmap (Original functionality) // ================================================================ let heatmapKind = 'activity'; let liveSource = true; // true = LIVE_RUNTIME, false = SNAPSHOT let refreshInterval = null; let heatmapInterval = null; let liveProjectionInterval = null; let experimentRunActive = false; let sharedExperimentMode = null; function renderExperimentRunFooter() { const vitals = document.querySelector('.footer-vitals'); if (!vitals) return; vitals.classList.toggle('is-test-running', experimentRunActive); if (!experimentRunActive) return; setText('footer-activity-value', 'Testlauf'); setText('footer-spikes-value', 'Testlauf'); const activityBar = $('footer-activity-bar'); const spikesBar = $('footer-spikes-bar'); if (activityBar) activityBar.style.width = '58%'; if (spikesBar) spikesBar.style.width = '42%'; } document.addEventListener('brain5d:experiment-progress', (event) => { const detail = event.detail || {}; experimentRunActive = Boolean(detail.active); document.body.dataset.experimentWorkflowActive = String(experimentRunActive); const footer = $('footer-experiment'); const state = $('footer-experiment-state'); const id = $('footer-experiment-id'); const progress = $('footer-experiment-progress'); if (footer) footer.dataset.active = String(experimentRunActive); if (state) state.textContent = detail.active ? 'running' : detail.label || 'inactive'; if (id && detail.experimentId) id.textContent = detail.experimentId; if (progress) progress.textContent = detail.active ? `${detail.progress ?? 0}% · ${detail.label || 'laufend'}` : detail.label || 'kein Lauf'; renderExperimentRunFooter(); if (!experimentRunActive && !detail.preserveFooter) dashboardStore.refresh().catch(() => {}); }); /** * Render system status from the central dashboard store. * @param {object} state */ function renderStatus(state) { const data = state; const system = data.system || {}; const storage = data.storage || {}; const learning = data.learning || {}; const selfOrg = data.self_organization || {}; const homeostasis = data.homeostasis || {}; renderOverviewCommandCenter(state); renderWorkspaceSummaries(state); // System metrics (always measured, real values) setText('tick', formatNumber(system.tick)); setText('neurons', formatNumber(system.neurons)); setText('synapses', formatNumber(system.synapses)); setText('spikes', formatNumber(system.spikes_total)); setText('core-ms', formatFloat(system.core_step_ms, 3)); setText('energy', formatFloat(system.mean_energy, 3)); // Storage metrics — distinguish disabled/unavailable from measured zero. if (storage.available === false) { setText('deltas', '—'); setText('bytes', '—'); setText('write-ms', '—'); setText('commit-ms', '—'); setText('journal-size', '—'); setText('drops', '—'); setText('queue-badge', 'disabled'); const fill = $('queue-fill'); if (fill) fill.style.width = '0%'; } else { setText('deltas', formatNumber(storage.deltas_written)); setText('bytes', formatBytes(storage.bytes_written)); setText('write-ms', formatMetricUnit(storage.write_latency_ms, 'ms', 3)); setText('commit-ms', formatMetricUnit(storage.commit_latency_ms, 'ms', 3)); setText('journal-size', formatBytes(storage.journal_size_bytes)); setText('drops', formatNumber(storage.dropped_batches)); const depth = storage.queue_depth; const capacity = storage.queue_capacity; if (depth === null || depth === undefined || capacity === null || capacity === undefined) { setText('queue-badge', '—'); const fill = $('queue-fill'); if (fill) fill.style.width = '0%'; } else { setText('queue-badge', `${depth} / ${capacity}`); const fill = $('queue-fill'); if (fill) { const percent = capacity > 0 ? Math.min(100, (depth / capacity) * 100) : 0; fill.style.width = `${percent}%`; } } } // Learning metrics setText('stdp', formatNumber(learning.stdp_updates)); setText('reward-updates', formatNumber(learning.reward_updates)); setText('rewards', `${formatNumber(learning.rewards_applied)} / ${formatNumber(learning.rewards_received)}`); setText('pending', formatNumber(learning.pending_rewards)); setText('learning-ms', formatMetricUnit(learning.update_ms, 'ms', 3)); setText('learning-engine-state', learning.engine_attached ? 'attached' : 'unavailable'); setText('learning-engine', learning.engine_attached ? 'ACTIVE' : 'UNAVAILABLE'); setText('learning-stdp-state', learning.stdp_enabled ? (learning.stdp_updates > 0 ? 'ACTIVE' : 'ARMED') : 'DISABLED'); setText('learning-eligibility-state', learning.eligibility_enabled ? 'ACTIVE' : 'DISABLED'); setText('learning-reward-state', learning.reward_enabled ? (learning.reward_updates > 0 ? 'ACTIVE' : 'ARMED') : 'DISABLED'); // Homeostasis if (homeostasis.enabled === false) { setText('homeostasis-state', 'disabled by config'); setText('target-rate', '—'); setText('mean-rate', '—'); setText('rate-error', '—'); setText('threshold-adaptation', '—'); setText('energy-error', '—'); setText('active-neurons', '—'); } else { setText('homeostasis-state', 'aktiv'); setText('target-rate', formatMetricUnit(homeostasis.target_rate_hz, 'Hz', 3)); setText('mean-rate', formatMetricUnit(homeostasis.mean_rate_hz || homeostasis.actual_rate_hz, 'Hz', 3)); setText('rate-error', formatMetricUnit(homeostasis.mean_rate_error_hz || homeostasis.rate_error_hz, 'Hz', 3)); setText('threshold-adaptation', formatFloat(homeostasis.mean_threshold_adaptation, 4)); setText('energy-error', formatFloat(homeostasis.mean_energy_error, 4)); setText('active-neurons', formatNumber(homeostasis.active_neurons)); } // Self-organization if (selfOrg.available === false) { setText('neurons-created', '—'); setText('neurons-removed', '—'); setText('synapses-created', '—'); setText('synapses-pruned', '—'); } else { setText('neurons-created', formatNumber(selfOrg.neurons_created)); setText('neurons-removed', formatNumber(selfOrg.neurons_removed)); setText('synapses-created', formatNumber(selfOrg.synapses_created)); setText('synapses-pruned', formatNumber(selfOrg.synapses_pruned)); } // Status badge const statusEl = $('system-status'); setText('footer-version', data.version || 'unknown'); const network = data.network || {}; const neuronCount = network.neuron_count ?? system.neurons; const activity = network.active_neurons != null && neuronCount ? Number(network.active_neurons) / Number(neuronCount) : null; const spikesPerTick = network.spikes_per_tick != null ? Number(network.spikes_per_tick) : null; const spikes = spikesPerTick != null ? spikesPerTick : (system.spikes_total != null ? Number(system.spikes_total) : null); const pressureInputs = [system.cpu_percent, system.memory_percent] .filter((value) => value != null) .map(Number); const pressure = pressureInputs.length ? Math.max(...pressureInputs) / 100 : null; setText('footer-activity-value', activity == null ? '—' : `${(activity * 100).toFixed(1)}%`); setText('footer-spikes-value', spikes == null ? '—' : formatNumber(spikes)); setText('footer-pressure-value', pressure == null ? '—' : `${Math.min(100, pressure * 100).toFixed(1)}%`); const activityBar = $('footer-activity-bar'); const spikesBar = $('footer-spikes-bar'); const pressureBar = $('footer-pressure-bar'); if (activityBar) activityBar.style.width = `${Math.min(100, Math.max(0, (activity || 0) * 100))}%`; if (spikesBar) spikesBar.style.width = `${spikesPerTick == null ? 0 : Math.min(100, Math.max(0, spikesPerTick * 100))}%`; if (pressureBar) pressureBar.style.width = `${pressure == null ? 0 : Math.min(100, Math.max(0, pressure * 100))}%`; renderExperimentRunFooter(); if (statusEl) { statusEl.textContent = `${data.status || 'idle'} · ${data.version || 'unknown'}`; const workerFailed = storage.worker_failed; statusEl.className = workerFailed === true ? 'status-pill error' : (workerFailed === false ? 'status-pill online' : 'status-pill'); } // Integration status badges (dashboard tab) refreshIntegrationStatus(); // Structural live loop status refreshLiveLoopStatus(); } /** * Compatibility shim: refresh system status via the central store. */ async function refreshStatus() { await dashboardStore.refresh(); } /** * Refresh the structural live loop status from the dedicated artifact endpoint. * * Reads ``/api/structural/live-loop`` and renders each of the 11 proofs with * its own status label. This keeps the visual loop honest: every step is * evidenced from the artifact, not inferred from the high-level gate status. */ async function refreshLiveLoopStatus() { const stepIds = ['ll-adapter', 'll-signal', 'll-policy', 'll-proposal', 'll-coordinator', 'll-approval', 'll-mutation', 'll-journal', 'll-undo', 'll-replay']; try { const r = await fetch('/api/structural/live-loop', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); const available = data.available === true; const proofs = data.proofs || {}; const allPassed = available && Object.values(proofs).every(v => v === true); const badge = $('live-loop-badge'); if (badge) { badge.textContent = allPassed ? '✅ VERIFIED' : (available ? '⏳ pending' : '❌ missing'); badge.className = `gate-badge ${allPassed ? 'passed' : (available ? 'pending' : 'failed')}`; } const meta = $('live-loop-meta'); if (meta) { if (!available) { meta.textContent = '⏳ Run test_structural_live_loop.py to generate verification artifact'; } else { const passedCount = Object.values(proofs).filter(v => v === true).length; const totalCount = Object.keys(proofs).length; meta.textContent = `✅ ${passedCount}/${totalCount} proofs passed · ${data.tested_tree_digest ? 'tree digest verified' : 'no digest'}`; } } // Map each UI step to the proof(s) it represents. const stepProofMap = { 'll-adapter': ['production_adapter_attached'], 'll-signal': ['real_signal_generated'], 'll-policy': ['policy_received_real_signal'], 'll-proposal': ['proposal_published', 'proposal_non_mutating'], 'll-coordinator': [], 'll-approval': ['reject_non_mutating', 'approve_single_mutation'], 'll-mutation': ['journal_linked_to_proposal', 'runtime_continues_after_mutation'], 'll-journal': [], 'll-undo': ['undo_restores_topology'], 'll-replay': ['journal_reopen_replay_identity'], }; const proofLabels = { production_adapter_attached: 'attached', real_signal_generated: 'real signal', policy_received_real_signal: 'received signal', proposal_published: 'published', proposal_non_mutating: 'non-mutating', reject_non_mutating: 'reject verified', approve_single_mutation: 'approve verified', journal_linked_to_proposal: 'linked record', runtime_continues_after_mutation: 'runtime continues', undo_restores_topology: 'topology restored', journal_reopen_replay_identity: 'replay identity', }; for (const stepId of stepIds) { const el = $(stepId); if (!el) continue; const statusEl = el.querySelector('.live-loop-status'); const proofKeys = stepProofMap[stepId] || []; if (!available) { el.className = 'live-loop-step ll-pending'; if (statusEl) statusEl.textContent = '⏳ pending'; continue; } const stepPassed = proofKeys.length === 0 || proofKeys.every(k => proofs[k] === true); const proofResults = proofKeys.map(k => ({ key: k, passed: proofs[k] === true })); if (stepPassed) { el.className = 'live-loop-step ll-passed'; if (statusEl) { if (proofKeys.length === 0) { statusEl.textContent = '✅ canonical instance'; } else if (proofKeys.length === 1) { statusEl.textContent = `✅ ${proofLabels[proofKeys[0]] || proofKeys[0]}`; } else { statusEl.textContent = `✅ ${proofKeys.length} proofs`; } } } else { el.className = 'live-loop-step ll-pending'; if (statusEl) { const failed = proofResults.filter(p => !p.passed); statusEl.textContent = `⏳ ${failed.map(p => proofLabels[p.key] || p.key).join(', ')}`; } } } } catch { for (const id of stepIds) { const el = $(id); if (el) { el.className = 'live-loop-step ll-pending'; const statusEl = el.querySelector('.live-loop-status'); if (statusEl) statusEl.textContent = '⏳ pending'; } } const badge = $('live-loop-badge'); if (badge) { badge.textContent = 'offline'; badge.className = 'gate-badge failed'; } } } /** * Refresh the integration status badges on the dashboard tab. * * Uses the real backend endpoint /api/integration/status (Phase 14) * instead of the previous frontend-only heuristic that hardcoded * int-tests to false. */ async function refreshIntegrationStatus() { const itemIds = ['int-bridge', 'int-controller', 'int-structural', 'int-snapshots', 'int-research', 'int-tests']; const nameToId = { 'Bridge': 'int-bridge', 'Controller': 'int-controller', 'Structural': 'int-structural', 'Snapshot': 'int-snapshots', 'Research': 'int-research', 'Tests': 'int-tests', }; try { const r = await fetch('/api/integration/status', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); const items = data.items || []; let passed = 0; for (const item of items) { const id = nameToId[item.name]; if (!id) continue; const el = $(id); if (!el) continue; const status = item.status || 'pending'; const cls = status === 'passed' ? 'int-passed' : status === 'disabled' ? 'int-disabled' : status === 'stale' ? 'int-stale' : status === 'failed' ? 'int-failed' : 'int-pending'; el.className = `integration-item clickable ${cls}`; el.title = item.message || ''; if (status === 'passed') passed++; } const badge = $('integration-badge'); if (badge) { const overall = data.overall || 'pending'; const label = overall === 'passed' ? `${passed}/${data.total} passed` : overall === 'stale' ? `STALE (${data.stale})` : overall === 'failed' ? `FAILED (${data.failed})` : `${passed}/${data.total} passed`; badge.textContent = label; badge.className = overall === 'passed' ? 'gate-badge passed' : overall === 'stale' ? 'gate-badge stale' : overall === 'failed' ? 'gate-badge failed' : 'gate-badge pending'; } } catch { for (const id of itemIds) { const el = $(id); if (el) el.className = 'integration-item int-failed'; } const badge = $('integration-badge'); if (badge) { badge.textContent = 'offline'; badge.className = 'gate-badge failed'; } } } /** * Refresh snapshot info from /api/snapshot-info */ async function refreshSnapshotInfo() { try { const response = await fetch('/api/snapshot-info', { cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const info = await readJson(response); if (info.active) { setText('snapshot-file', info.path || '—'); setText('snapshot-tick', info.tick != null ? formatNumber(info.tick) : '—'); setText('snapshot-size', info.size_bytes != null ? formatBytes(info.size_bytes) : '—'); setText('snapshot-status', '✅ aktiv'); } else { setText('snapshot-file', '—'); setText('snapshot-tick', '—'); setText('snapshot-size', '—'); setText('snapshot-status', '⏳ initializing...'); } } catch { setText('snapshot-status', '⚠️ offline'); } } /** * Refresh heatmap from /api/heatmap */ async function refreshHeatmap() { const canvas = $('heatmap'); if (!canvas) return; try { const response = await fetch( `/api/heatmap?kind=${encodeURIComponent(heatmapKind)}`, { cache: 'no-store' } ); if (!response.ok) { const data = await readJson(response); setText('heatmap-meta', data.error || `Heatmap HTTP ${response.status}`); return; } drawHeatmap(await readJson(response)); } catch (error) { setText('heatmap-meta', '⚠️ Heatmap nicht erreichbar.'); } } /** * Draw heatmap on canvas */ function drawHeatmap(payload) { const canvas = $('heatmap'); if (!canvas) return; const ctx = canvas.getContext('2d'); const rows = payload.values || []; if (!rows.length || !rows[0]?.length) { setText('heatmap-meta', 'Keine Heatmap-Daten verfügbar.'); return; } // Filter out null/undefined values for range computation const valid = rows.flat().filter(v => v !== null && v !== undefined && Number.isFinite(v)); const min = valid.length > 0 ? Math.min(...valid) : 0; const max = valid.length > 0 ? Math.max(...valid) : 0; const width = rows[0].length; const height = rows.length; const cellW = canvas.width / width; const cellH = canvas.height / height; ctx.clearRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const val = rows[y][x]; if (val === null || val === undefined) { // No-data cell: draw a neutral pattern ctx.fillStyle = 'rgba(40, 50, 65, 0.4)'; ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH)); // Draw a subtle crosshatch to indicate "no data" ctx.strokeStyle = 'rgba(60, 70, 85, 0.3)'; ctx.lineWidth = 0.5; const cx = x * cellW; const cy = y * cellH; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + cellW, cy + cellH); ctx.moveTo(cx + cellW, cy); ctx.lineTo(cx, cy + cellH); ctx.stroke(); } else { ctx.fillStyle = heatmapColor(val, min, max); ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH)); } } } setText( 'heatmap-meta', `${payload.source === 'live_runtime' ? 'LIVE' : 'SNAPSHOT'} · ${payload.kind || heatmapKind} · Tick ${payload.tick || 0} · ${payload.sample_count || 0} Samples · ${formatFloat(min, 4)}…${formatFloat(max, 4)}` ); // Store telemetry status for badge updates const metaEl = $('heatmap-meta'); if (metaEl && payload.telemetry && payload.telemetry.status) { metaEl.dataset.telemetryStatus = payload.telemetry.status; } // Also draw the 5D projection if available draw5DProjection(payload); } /** * Refresh live projection from /api/live/projection (LIVE_RUNTIME source) */ async function refreshLiveProjection() { const canvas = $('heatmap'); if (!canvas) return; if (!liveSource) return; // Only when live mode is active try { const response = await fetch( `/api/live/projection?kind=${encodeURIComponent(heatmapKind)}&resolution=${encodeURIComponent($('projection-resolution')?.value || '50')}`, { cache: 'no-store' } ); if (!response.ok) { // Fall back to snapshot heatmap if live unavailable if (liveSource) { liveSource = false; updateSourceBadge(); } return; } const data = await readJson(response); drawHeatmap(data); updateSourceBadge(data.telemetry?.status); } catch { // Silently fall back — snapshot will be used on next interval } } /** * Update the source badge in the UI based on telemetry status */ function updateSourceBadge(telemetryStatus) { const badge = $('source-badge'); if (!badge) return; if (!liveSource) { badge.textContent = 'SNAPSHOT'; badge.className = 'badge badge-snapshot'; return; } // Use explicit status parameter, fall back to DOM dataset const status = telemetryStatus || (document.getElementById('heatmap-meta')?.dataset?.telemetryStatus); if (status === 'stale') { badge.textContent = 'STALE'; badge.className = 'badge badge-stale'; return; } if (status === 'unavailable') { badge.textContent = 'UNAVAILABLE'; badge.className = 'badge badge-unavailable'; return; } badge.textContent = 'LIVE'; badge.className = 'badge badge-live'; } /** * Draw a 5D projection visualization alongside the heatmap. * Uses a 3D perspective projection of the 5D network space. */ function draw5DProjection(payload) { const canvas = document.getElementById('projection-5d'); if (!canvas) return; const ctx = canvas.getContext('2d'); const values = payload.values || []; if (!values.length) return; ctx.clearRect(0, 0, canvas.width, canvas.height); const rows = values.length; const cols = values[0]?.length || 1; // Create a pseudo-3D isometric projection const cx = canvas.width / 2; const cy = canvas.height / 2 + 20; const scale = Math.min(canvas.width, canvas.height) * 0.35 / Math.max(rows, cols); const flat = values.flat().filter(v => v !== null && v !== undefined && Number.isFinite(v)); if (flat.length === 0) { ctx.fillStyle = '#4a6a8a'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Keine Daten', cx, cy); return; } const min = Math.min(...flat); const max = Math.max(...flat); const range = max - min || 1; // Draw base grid (isometric floor) ctx.strokeStyle = 'rgba(64, 224, 208, 0.06)'; ctx.lineWidth = 0.5; const gridStep = 4; for (let y = 0; y < rows; y += gridStep) { for (let x = 0; x < cols; x += gridStep) { const gx1 = cx + (x - y) * scale * 0.7; const gy1 = cy + (x + y) * scale * 0.35; // Draw small grid cross ctx.beginPath(); ctx.arc(gx1, gy1, 1.5, 0, Math.PI * 2); ctx.fillStyle = 'rgba(64, 224, 208, 0.15)'; ctx.fill(); } } // Draw grid points with height based on value for (let y = 0; y < rows; y += 2) { for (let x = 0; x < cols; x += 2) { const val = values[y]?.[x]; if (val === null || val === undefined) continue; const normalized = (val - min) / range; // Isometric projection const isoX = (x - y) * scale * 0.7; const isoY = (x + y) * scale * 0.35 - normalized * scale * 2; const size = 2 + normalized * 5; const hue = 210 - normalized * 195; const light = 20 + normalized * 55; ctx.beginPath(); ctx.arc(cx + isoX, cy + isoY, size, 0, Math.PI * 2); ctx.fillStyle = `hsl(${hue}, 85%, ${light}%)`; ctx.fill(); // Subtle glow for high values if (normalized > 0.6) { ctx.beginPath(); ctx.arc(cx + isoX, cy + isoY, size * 2.5, 0, Math.PI * 2); ctx.fillStyle = `hsla(${hue}, 85%, ${light}%, 0.15)`; ctx.fill(); } } } // Draw connecting lines for structure ctx.strokeStyle = 'rgba(64, 224, 208, 0.08)'; ctx.lineWidth = 0.5; for (let y = 0; y < rows - 2; y += 3) { for (let x = 0; x < cols - 2; x += 3) { const v1 = values[y]?.[x]; const v2 = values[y]?.[x + 2]; const v3 = values[y + 2]?.[x]; if (v1 === null || v1 === undefined || v2 === null || v2 === undefined || v3 === null || v3 === undefined) continue; const n1 = (v1 - min) / range; const n2 = (v2 - min) / range; const n3 = (v3 - min) / range; const x1 = cx + (x - y) * scale * 0.7; const y1 = cy + (x + y) * scale * 0.35 - n1 * scale * 2; const x2 = cx + ((x + 2) - y) * scale * 0.7; const y2 = cy + ((x + 2) + y) * scale * 0.35 - n2 * scale * 2; const x3 = cx + (x - (y + 2)) * scale * 0.7; const y3 = cy + (x + (y + 2)) * scale * 0.35 - n3 * scale * 2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x3, y3); ctx.stroke(); } } // Draw axis labels ctx.fillStyle = 'rgba(142, 178, 193, 0.5)'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; // X axis ctx.fillText('X →', cx + cols * scale * 0.35, cy + cols * scale * 0.17 + 12); // Y axis ctx.fillText('Y →', cx - rows * scale * 0.35, cy + rows * scale * 0.17 + 12); // Z legend (value height) ctx.fillText(`Z: ${min.toFixed(2)} … ${max.toFixed(2)}`, cx, 14); // Draw 5D info const dims = payload.dimensions || []; if (dims.length >= 5) { ctx.fillStyle = 'rgba(142, 178, 193, 0.35)'; ctx.font = '9px monospace'; ctx.textAlign = 'left'; ctx.fillText(`5D: ${dims.join('×')}`, 8, canvas.height - 8); } } // ================================================================ // IO-FLUSS REFRESH // ================================================================ let ioFlowInterval = null; /** * Refresh IO-Fluss visualization from /api/live/io-flow */ async function refreshIOFlow() { try { const r = await fetch('/api/live/io-flow', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); // Input layer setText('io-input-count', formatNumber(data.input_count)); setText('io-input-rate', formatFloat(data.input_mean_rate, 4)); const inputFill = document.getElementById('io-input-fill'); if (inputFill) inputFill.style.width = Math.min(100, data.input_mean_rate * 200) + '%'; // Hidden layer setText('io-hidden-count', formatNumber(data.hidden_count)); setText('io-hidden-rate', formatFloat(data.hidden_mean_rate, 4)); const hiddenFill = document.getElementById('io-hidden-fill'); if (hiddenFill) hiddenFill.style.width = Math.min(100, data.hidden_mean_rate * 200) + '%'; // Output layer setText('io-output-count', formatNumber(data.output_count)); setText('io-output-rate', formatFloat(data.output_mean_rate, 4)); const outputFill = document.getElementById('io-output-fill'); if (outputFill) outputFill.style.width = Math.min(100, data.output_mean_rate * 200) + '%'; // Badge const badge = document.getElementById('io-flow-badge'); if (badge) { if (data.propagation_active) { badge.textContent = '✅ Signalfluss aktiv'; badge.className = 'gate-badge passed'; } else { badge.textContent = '⏳ Signal abgebrochen'; badge.className = 'gate-badge pending'; } } // Meta const meta = document.getElementById('io-flow-meta'); if (meta) { meta.textContent = `Tick ${data.current_tick} · Input ${data.input_mean_rate.toFixed(4)} → Hidden ${data.hidden_mean_rate.toFixed(4)} → Output ${data.output_mean_rate.toFixed(4)} · ${data.source}`; } setText('footer-input-value', `${formatNumber(data.input_count)} · ${formatFloat(data.input_mean_rate, 3)}`); setText('footer-output-value', `${formatNumber(data.output_count)} · ${formatFloat(data.output_mean_rate, 3)}`); setText('footer-io-state', data.propagation_active ? 'Signalfluss aktiv' : 'kein aktueller Signalfluss'); } catch (e) { const badge = document.getElementById('io-flow-badge'); if (badge) { badge.textContent = '⚠️ offline'; badge.className = 'gate-badge failed'; } const meta = document.getElementById('io-flow-meta'); if (meta) meta.textContent = '⚠️ IO-Fluss nicht verfügbar'; setText('footer-input-value', '—'); setText('footer-output-value', '—'); setText('footer-io-state', 'I/O offline'); } } // ================================================================ // POPULATION OVERVIEW REFRESH // ================================================================ let populationInterval = null; /** * Refresh population overview from /api/live/population */ async function refreshPopulation() { try { const r = await fetch('/api/live/population', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); // E/I ratio badge const badge = document.getElementById('ei-ratio-badge'); if (badge) { const ratio = data.ei_ratio; if (typeof ratio === 'number') { badge.textContent = `E/I: ${ratio.toFixed(2)}`; badge.className = `gate-badge ${ratio > 0.5 && ratio < 3.0 ? 'passed' : 'stale'}`; } else { badge.textContent = 'E/I: unavailable'; badge.className = 'gate-badge pending'; } } // Population cards const grid = document.getElementById('population-grid'); if (!grid) return; if (!data.populations || data.populations.length === 0) { grid.innerHTML = '
Keine Populationsdaten verfügbar.
'; } else { grid.innerHTML = data.populations.map(p => { const activePct = (p.active_fraction * 100).toFixed(1); const barColor = p.name.includes('inhib') ? '#ff5d73' : p.name.includes('excit') ? '#40e0a8' : p.name.includes('sensory') ? '#4a7cf7' : p.name.includes('motor') ? '#f0a840' : '#8899bb'; return `
${escapeHtml(p.name)} ${formatNumber(p.count)}
Rate
${formatFloat(p.mean_rate, 4)} spikes/tick
Energie
${formatFloat(p.mean_energy, 4)}
Membran V
${formatFloat(p.mean_v, 3)}
Aktiv
${formatNumber(p.active_count)} / ${formatNumber(p.count)} (${activePct}%)
`; }).join(''); } // Meta const meta = document.getElementById('population-meta'); if (meta) { meta.textContent = `Tick ${data.current_tick} · E: ${data.total_excitatory} / I: ${data.total_inhibitory} · ${data.source}`; } } catch (e) { const badge = document.getElementById('ei-ratio-badge'); if (badge) { badge.textContent = 'E/I: —'; badge.className = 'gate-badge'; } const grid = document.getElementById('population-grid'); if (grid) { grid.innerHTML = '
⚠️ Populationsdaten nicht verfügbar.
'; } } } /** * Initialize dashboard refresh cycles */ function initDashboard() { // Central state store drives the OVERVIEW tab and health drawer. dashboardStore.subscribe(renderStatus); dashboardStore.start(); // Initialize health / problems drawer initHealthDrawer(dashboardStore); // Visualizations still poll their own endpoints (network tab) refreshHeatmap(); refreshLiveProjection(); refreshSnapshotInfo(); refreshLiveLoopStatus(); refreshIOFlow(); refreshPopulation(); // Set up intervals if (refreshInterval) clearInterval(refreshInterval); if (heatmapInterval) clearInterval(heatmapInterval); if (liveProjectionInterval) clearInterval(liveProjectionInterval); if (ioFlowInterval) clearInterval(ioFlowInterval); if (populationInterval) clearInterval(populationInterval); heatmapInterval = setInterval(refreshHeatmap, pollInterval(5000)); liveProjectionInterval = setInterval(refreshLiveProjection, pollInterval(500)); setInterval(refreshSnapshotInfo, pollInterval(3000)); setInterval(refreshLiveLoopStatus, pollInterval(5000)); ioFlowInterval = setInterval(refreshIOFlow, pollInterval(2000)); populationInterval = setInterval(refreshPopulation, pollInterval(2000)); // Heatmap kind buttons $$('button[data-kind]').forEach(button => { button.addEventListener('click', () => { heatmapKind = button.dataset.kind; $$('button[data-kind]').forEach(b => b.classList.remove('active')); button.classList.add('active'); if (liveSource) { refreshLiveProjection(); } else { refreshHeatmap(); } }); }); // Live / Snapshot toggle const liveToggle = $('live-toggle'); if (liveToggle) { liveToggle.addEventListener('click', () => { liveSource = !liveSource; updateSourceBadge(); if (liveSource) { refreshLiveProjection(); } else { refreshHeatmap(); } }); } updateSourceBadge(); } // ================================================================ // DYNAMICS TAB (Spike Raster, Rate Histogram, Layer Explorer) // ================================================================ let dynamicsInterval = null; let layerExplorerInterval = null; function initDynamicsTab() { console.log('📈 Dynamics tab initializing...'); refreshSpikeRaster(); refreshRateHistogram(); refreshLayerExplorer(); // Set up intervals if (dynamicsInterval) clearInterval(dynamicsInterval); if (layerExplorerInterval) clearInterval(layerExplorerInterval); dynamicsInterval = setInterval(refreshSpikeRaster, pollInterval(2000)); dynamicsInterval = setInterval(refreshRateHistogram, pollInterval(2000)); layerExplorerInterval = setInterval(refreshLayerExplorer, pollInterval(3000)); // Layer slider controls const slider = document.getElementById('layer-slider'); const dimSelect = document.getElementById('layer-dim'); const kindSelect = document.getElementById('layer-kind'); const layerVal = document.getElementById('layer-value'); if (slider) { slider.addEventListener('input', () => { if (layerVal) layerVal.textContent = slider.value; }); slider.addEventListener('change', refreshLayerExplorer); } if (dimSelect) dimSelect.addEventListener('change', refreshLayerExplorer); if (kindSelect) kindSelect.addEventListener('change', refreshLayerExplorer); const projectionResolution = $('projection-resolution'); const histogramBins = $('histogram-bins'); const projectionSamples = $('projection-samples'); const bindRange = (input, outputId, refresh) => { if (!input) return; input.addEventListener('input', () => setText(outputId, input.value)); input.addEventListener('change', refresh); }; bindRange(projectionResolution, 'projection-resolution-value', refreshLiveProjection); bindRange(histogramBins, 'histogram-bins-value', refreshRateHistogram); bindRange(projectionSamples, 'projection-samples-value', loadProjection); console.log('✅ Dynamics tab ready'); } // ================================================================ // SPIKE RASTER // ================================================================ async function refreshSpikeRaster() { try { const r = await fetch('/api/live/raster', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); drawSpikeRaster(data); const badge = document.getElementById('raster-badge'); if (badge) { badge.textContent = `${data.sample_count}/${data.total_neurons} Neuronen`; badge.className = 'gate-badge passed'; } const meta = document.getElementById('raster-meta'); if (meta) meta.textContent = `Tick ${data.tick} · Fenster: ${data.window_ticks} Ticks · ${data.sample_count} aktive Neuronen von ${data.total_neurons}`; } catch { const badge = document.getElementById('raster-badge'); if (badge) { badge.textContent = 'offline'; badge.className = 'gate-badge failed'; } } } function drawSpikeRaster(data) { const canvas = document.getElementById('spike-raster'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const nids = data.neuron_ids || []; const ticks = data.spike_ticks || []; if (!nids.length || !ticks.length) { ctx.fillStyle = '#4a6a8a'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Keine Spike-Daten im aktuellen Fenster', canvas.width/2, canvas.height/2); return; } const windowTicks = data.window_ticks || 100; const tick = data.tick || 0; const tickMin = tick - windowTicks; const tickMax = tick; const neuronCount = nids.length; const idToRow = {}; nids.forEach((id, i) => { idToRow[id] = i; }); const margin = { top: 20, right: 20, bottom: 30, left: 50 }; const plotW = canvas.width - margin.left - margin.right; const plotH = canvas.height - margin.top - margin.bottom; // Background ctx.fillStyle = '#050d14'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw grid lines ctx.strokeStyle = 'rgba(142, 178, 193, 0.06)'; ctx.lineWidth = 0.5; for (let i = 0; i < 10; i++) { const y = margin.top + (plotH / 10) * i; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(margin.left + plotW, y); ctx.stroke(); } // Draw spikes ctx.fillStyle = 'rgba(64, 224, 208, 0.6)'; for (let i = 0; i < ticks.length; i++) { const t = ticks[i]; const nid = nids[i % nids.length]; const row = idToRow[nid]; if (row === undefined) continue; const x = margin.left + ((t - tickMin) / Math.max(1, tickMax - tickMin)) * plotW; const y = margin.top + (row / Math.max(1, neuronCount)) * plotH; ctx.fillRect(x - 0.5, y - 1, 2, 2); } // Labels ctx.fillStyle = 'rgba(142, 178, 193, 0.5)'; ctx.font = '11px monospace'; ctx.textAlign = 'center'; ctx.fillText('Tick →', margin.left + plotW/2, canvas.height - 5); ctx.textAlign = 'right'; ctx.fillText('Neuron ↑', margin.left - 5, margin.top + 12); ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(142, 178, 193, 0.3)'; ctx.font = '9px monospace'; ctx.fillText(tickMin, margin.left, canvas.height - 8); ctx.textAlign = 'right'; ctx.fillText(tickMax, margin.left + plotW, canvas.height - 8); } // ================================================================ // RATE HISTOGRAM // ================================================================ async function refreshRateHistogram() { try { const bins = $('histogram-bins')?.value || '30'; const r = await fetch(`/api/live/histogram?bins=${encodeURIComponent(bins)}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); drawRateHistogram(data); const stats = document.getElementById('histogram-stats'); if (stats) { stats.textContent = `μ=${data.mean_rate.toFixed(4)} σ=${data.std_rate.toFixed(4)}`; stats.className = 'gate-badge passed'; } const meta = document.getElementById('histogram-meta'); if (meta) meta.textContent = `Tick ${data.current_tick} · ${data.active_count} aktiv / ${data.silent_count} stumm · Median ${data.median_rate.toFixed(4)}`; } catch { const stats = document.getElementById('histogram-stats'); if (stats) { stats.textContent = 'offline'; stats.className = 'gate-badge failed'; } } } function drawRateHistogram(data) { const canvas = document.getElementById('rate-histogram'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const bins = data.bins || []; const counts = data.counts || []; if (!counts.length) { ctx.fillStyle = '#4a6a8a'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Keine Histogramm-Daten', canvas.width/2, canvas.height/2); return; } const margin = { top: 20, right: 20, bottom: 35, left: 60 }; const plotW = canvas.width - margin.left - margin.right; const plotH = canvas.height - margin.top - margin.bottom; ctx.fillStyle = '#050d14'; ctx.fillRect(0, 0, canvas.width, canvas.height); const maxCount = Math.max(...counts, 1); const barW = plotW / counts.length; // Draw bars for (let i = 0; i < counts.length; i++) { const x = margin.left + i * barW; const h = (counts[i] / maxCount) * plotH; const y = margin.top + plotH - h; const normalized = counts[i] / maxCount; const hue = 210 - normalized * 195; ctx.fillStyle = `hsl(${hue}, 85%, ${30 + normalized * 40}%)`; ctx.fillRect(x + 1, y, barW - 2, h); } // Mean line if (data.mean_rate > 0 && bins.length > 1) { const maxRate = bins[bins.length - 1]; const meanX = margin.left + (data.mean_rate / maxRate) * plotW; ctx.strokeStyle = 'rgba(255, 159, 28, 0.7)'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(meanX, margin.top); ctx.lineTo(meanX, margin.top + plotH); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = 'rgba(255, 159, 28, 0.7)'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; ctx.fillText('μ=' + data.mean_rate.toFixed(3), meanX, margin.top - 4); } // Labels ctx.fillStyle = 'rgba(142, 178, 193, 0.5)'; ctx.font = '11px monospace'; ctx.textAlign = 'center'; ctx.fillText('Feuerrate (spikes/tick) →', margin.left + plotW/2, canvas.height - 5); ctx.textAlign = 'right'; ctx.fillText('Anzahl ↑', margin.left - 5, margin.top + 12); } // ================================================================ // 5D LAYER EXPLORER // ================================================================ async function refreshLayerExplorer() { try { const dimSelect = document.getElementById('layer-dim'); const slider = document.getElementById('layer-slider'); const kindSelect = document.getElementById('layer-kind'); if (!dimSelect || !slider) return; const dim = dimSelect.value; const layerVal = parseInt(slider.value, 10); const kind = kindSelect ? kindSelect.value : 'activity'; const r = await fetch(`/api/live/projection?kind=${encodeURIComponent(kind)}&resolution=40`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await readJson(r); drawLayerSlice(data, dim, layerVal); const badge = document.getElementById('layer-badge'); if (badge) badge.textContent = dim.toUpperCase(); const meta = document.getElementById('layer-meta'); if (meta) meta.textContent = `Tick ${data.tick ?? data.current_tick} · ${dim.toUpperCase()}=${layerVal} · ${data.kind || kind} · ${data.source}`; } catch { const meta = document.getElementById('layer-meta'); if (meta) meta.textContent = '⚠️ Layer-Daten nicht verfügbar'; } } function drawLayerSlice(payload, dim, layerValue) { const canvas = document.getElementById('layer-canvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const values = payload.values || []; if (!values.length) return; const rows = values.length; const cols = values[0]?.length || 1; const flat = values.flat().filter(v => v !== null && v !== undefined && Number.isFinite(v)); if (!flat.length) { ctx.fillStyle = '#4a6a8a'; ctx.font = '14px monospace'; ctx.textAlign = 'center'; ctx.fillText('Keine Daten', canvas.width/2, canvas.height/2); return; } const min = Math.min(...flat); const max = Math.max(...flat); const range = max - min || 1; const cellW = canvas.width / cols; const cellH = canvas.height / rows; for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { const val = values[y]?.[x]; if (val === null || val === undefined) { ctx.fillStyle = 'rgba(40, 50, 65, 0.4)'; ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH)); } else { const normalized = (val - min) / range; const hue = 210 - normalized * 195; const light = 16 + normalized * 50; ctx.fillStyle = `hsl(${hue}, 88%, ${light}%)`; ctx.fillRect(x * cellW, y * cellH, Math.ceil(cellW), Math.ceil(cellH)); } } } // Overlay ctx.fillStyle = 'rgba(142, 178, 193, 0.4)'; ctx.font = '10px monospace'; ctx.textAlign = 'left'; ctx.fillText(`${dim.toUpperCase()}=${layerValue}`, 8, 16); ctx.textAlign = 'right'; ctx.fillText(`${min.toFixed(2)} … ${max.toFixed(2)}`, canvas.width - 8, 16); } // ================================================================ // INSPECT TAB (real 5D network inspector, Phase 8/9) // ================================================================ let inspectInterval = null; function initInspectTab() { console.log('🔍 Inspect tab initializing...'); refreshNetworkSummary(); loadNeuronPage(); loadSynapsePage(); loadProjection(); const refreshBtn = $('inspect-refresh'); if (refreshBtn) refreshBtn.addEventListener('click', () => { refreshNetworkSummary(); loadNeuronPage(); loadSynapsePage(); loadProjection(); }); const neuronLoad = $('neuron-load'); if (neuronLoad) neuronLoad.addEventListener('click', loadNeuronPage); const synapseLoad = $('synapse-load'); if (synapseLoad) synapseLoad.addEventListener('click', loadSynapsePage); const projMode = $('projection-mode'); if (projMode) projMode.addEventListener('change', loadProjection); // Auto-refresh summary every 2s while inspect tab is active if (inspectInterval) clearInterval(inspectInterval); inspectInterval = setInterval(refreshNetworkSummary, pollInterval(2000)); console.log('✅ Inspect tab ready'); } async function refreshNetworkSummary() { try { const r = await fetch('/api/network/summary', { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const d = await readJson(r); setText('inspect-dimensions', (d.dimensions || []).join(' × ')); setText('inspect-neuron-count', formatNumber(d.neuron_count)); setText('inspect-synapse-count', formatNumber(d.synapse_count)); setText('inspect-input-count', formatNumber(d.input_count)); setText('inspect-output-count', formatNumber(d.output_count)); setText('inspect-active-neurons', formatNumber(d.active_neurons)); setText('inspect-silent-neurons', formatNumber(d.silent_neurons)); setText('inspect-queue-depth', formatNumber(d.queue_depth)); setText('inspect-tick', formatNumber(d.current_tick)); setText('inspect-total-spikes', formatNumber(d.total_spikes)); setText('inspect-mean-energy', formatFloat(d.mean_energy, 4)); setText('inspect-mean-v', formatFloat(d.mean_v, 4)); } catch { ['inspect-dimensions','inspect-neuron-count','inspect-synapse-count','inspect-input-count', 'inspect-output-count','inspect-active-neurons','inspect-silent-neurons','inspect-queue-depth', 'inspect-tick','inspect-total-spikes','inspect-mean-energy','inspect-mean-v'] .forEach(id => setText(id, '—')); } } async function loadNeuronPage() { const limit = parseInt($('neuron-limit')?.value || '200', 10); const offset = parseInt($('neuron-offset')?.value || '0', 10); const activeOnly = $('neuron-active-only')?.checked || false; try { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); if (activeOnly) params.set('active_only', 'true'); const r = await fetch(`/api/network/neurons?${params}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const d = await readJson(r); const body = $('neuron-table-body'); if (!body) return; const rows = d.neurons || []; body.innerHTML = rows.map(n => ` ${n.neuron_id} ${n.x1}${n.x2}${n.x3}${n.x4}${n.x5} ${formatFloat(n.v, 3)} ${formatFloat(n.u, 3)} ${formatFloat(n.energy, 4)} ${n.last_spike < 0 ? '—' : n.last_spike} ${n.spike_count} ${n.is_input ? 'I' : (n.is_output ? 'O' : '')} `).join(''); setText('neuron-page-meta', `${d.returned}/${d.total} (offset ${d.offset}, limit ${d.limit}) · source: ${d.source}`); } catch (e) { const body = $('neuron-table-body'); if (body) body.innerHTML = `⚠️ ${escapeHtml(e.message)}`; setText('neuron-page-meta', '—'); } } async function loadSynapsePage() { const limit = parseInt($('synapse-limit')?.value || '200', 10); const offset = parseInt($('synapse-offset')?.value || '0', 10); const sourceRaw = $('synapse-source')?.value; const minWeightRaw = $('synapse-min-weight')?.value; try { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); if (sourceRaw) params.set('source', sourceRaw); if (minWeightRaw) params.set('min_weight', minWeightRaw); const r = await fetch(`/api/network/synapses?${params}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const d = await readJson(r); const body = $('synapse-table-body'); if (!body) return; const rows = d.synapses || []; body.innerHTML = rows.map(s => ` ${s.source_id} ${s.target_id ?? '—'} ${formatFloat(s.weight, 4)} ${s.delay ?? '—'} ${formatFloat(s.eligibility, 4)} `).join(''); setText('synapse-page-meta', `${d.returned}/${d.total} (offset ${d.offset}, limit ${d.limit}) · source: ${d.source}`); } catch (e) { const body = $('synapse-table-body'); if (body) body.innerHTML = `⚠️ ${escapeHtml(e.message)}`; setText('synapse-page-meta', '—'); } } async function loadProjection() { const canvas = $('inspect-projection'); if (!canvas) return; const mode = $('projection-mode')?.value || 'activity'; const limit = $('projection-samples')?.value || '2000'; try { const r = await fetch(`/api/network/projection?limit=${encodeURIComponent(limit)}&mode=${encodeURIComponent(mode)}`, { cache: 'no-store' }); if (!r.ok) throw new Error(`HTTP ${r.status}`); const d = await readJson(r); drawInspectProjection(canvas, d); setText('projection-meta', `${d.label} · ${d.sample_count}/${d.total_count} samples · ${d.sampling_method} · source: ${d.source}`); } catch (e) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); setText('projection-meta', `⚠️ ${e.message}`); } } function drawInspectProjection(canvas, payload) { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const points = payload.points || []; if (!points.length) { setText('projection-meta', 'No projection data.'); return; } // Determine value range for colour mapping. const vals = points.map(p => p.value); const vMin = Math.min(...vals); const vMax = Math.max(...vals); const range = vMax - vMin || 1; // Spatial range for x,y,z (first three dims). const xs = points.map(p => p.x); const ys = points.map(p => p.y); const zs = points.map(p => p.z); const xMin = Math.min(...xs), xMax = Math.max(...xs); const yMin = Math.min(...ys), yMax = Math.max(...ys); const zMin = Math.min(...zs), zMax = Math.max(...zs); const cx = canvas.width / 2; const cy = canvas.height / 2; const scale = Math.min(canvas.width, canvas.height) * 0.35 / Math.max(xMax - xMin, yMax - yMin, zMax - zMin, 1); // Draw points: x,y as visible axes, z as depth (size), d4/d5 carried as data. for (const p of points) { const nx = (p.x - xMin) / Math.max(xMax - xMin, 1); const ny = (p.y - yMin) / Math.max(yMax - yMin, 1); const nz = (p.z - zMin) / Math.max(zMax - zMin, 1); const valN = (p.value - vMin) / range; // Isometric-ish projection: x,y visible, z shifts vertically. const px = cx + (nx - 0.5) * canvas.width * 0.8; const py = cy + (ny - 0.5) * canvas.height * 0.7 - nz * scale * 1.5; const size = 1.5 + valN * 4 + (p.is_input || p.is_output ? 2 : 0); const hue = 210 - valN * 195; const light = 20 + valN * 50; ctx.beginPath(); ctx.arc(px, py, size, 0, Math.PI * 2); ctx.fillStyle = `hsl(${hue}, 85%, ${light}%)`; ctx.fill(); // Highlight input/output cells. if (p.is_input || p.is_output) { ctx.strokeStyle = p.is_input ? '#40e0a8' : '#f0a840'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(px, py, size + 2, 0, Math.PI * 2); ctx.stroke(); } } // Legend for d4/d5 (carried as filter attributes, not visualised here). ctx.fillStyle = '#8899bb'; ctx.font = '11px monospace'; ctx.fillText('X,Y visible · Z=depth · D4/D5 in point data (filter TBD)', 10, canvas.height - 10); } // ================================================================ // CONTROL PANEL INITIALIZATION (sole owner) // ================================================================ function initControlPanel() { console.log('🎮 Control Panel initializing...'); const instance = new ControlPanel(); console.log('✅ Control Panel initialized'); return instance; } // ================================================================ // OPERATOR CONSOLE INITIALIZATION (sole owner) // ================================================================ function initOperatorConsole() { console.log('📟 Operator Console initializing...'); const instance = new OperatorConsole(); console.log('✅ Operator Console initialized'); return instance; } // ================================================================ // PARAMETER INSPECTOR INITIALIZATION (sole owner) // ================================================================ function initParameterInspector() { console.log('🔧 Parameter Inspector initializing...'); const instance = new ParameterInspector(); instance.refresh(); console.log('✅ Parameter Inspector initialized'); return instance; } /** * Refresh console status display */ async function refreshConsoleStatus() { try { const response = await fetch('/api/status', { cache: 'no-store' }); if (!response.ok) return; const data = await readJson(response); // Update console status badge const badge = $('b5d-state-badge'); if (badge) { const state = data.status || 'idle'; badge.textContent = state; badge.className = `status-badge status-${state}`; } // Update tick display const tickEl = $('b5d-tick-display'); if (tickEl) { tickEl.textContent = String(data.system?.tick || 0); } // Update metrics const system = data.system || {}; setText('metric-neurons', formatNumber(system.neurons)); setText('metric-synapses', formatNumber(system.synapses)); setText('metric-spikes', formatNumber(system.spikes_total)); // queue_depth is part of controller telemetry, surfaced via /api/control state; // here it is not present in /api/status.system, so show "—" not fake 0. setText('metric-queue', formatNumber(system.queue_depth)); // Load proposals loadProposals(); } catch { // Silently fail } } /** * Load structural proposals */ async function loadProposals() { try { const response = await fetch('/api/structural/proposals', { cache: 'no-store' }); if (!response.ok) return; const data = await readJson(response); const proposals = data.proposals || []; // Update badge count const countEl = $('proposals-count'); if (countEl) { countEl.textContent = `${proposals.length} pending`; countEl.className = `badge${proposals.length > 0 ? ' has-proposals' : ''}`; } // Render proposals const container = $('b5d-proposals'); if (!container) return; if (proposals.length === 0) { container.innerHTML = '
No pending proposals
'; return; } container.innerHTML = proposals.map(p => `
${escapeHtml(p.kind || 'unknown')} Neuron ${p.neuron_id || '?'} → ${p.target_id || '?'} ${((p.confidence || 0) * 100).toFixed(0)}% ${escapeHtml(p.reason || '')}
`).join(''); // Bind approve/reject events container.querySelectorAll('.btn-approve').forEach(btn => { btn.addEventListener('click', () => handleProposal(btn.dataset.id, 'approve')); }); container.querySelectorAll('.btn-reject').forEach(btn => { btn.addEventListener('click', () => handleProposal(btn.dataset.id, 'reject')); }); } catch (e) { // Silently fail } } /** * Handle proposal approval/rejection */ async function handleProposal(proposalId, action) { const output = $('console-output'); const time = new Date().toLocaleTimeString(); if (output) { const logEntry = document.createElement('div'); logEntry.className = 'log-entry log-info'; logEntry.innerHTML = `[${time}] ${action === 'approve' ? '✓' : '✗'} ${action}ing proposal ${proposalId}...`; output.appendChild(logEntry); output.scrollTop = output.scrollHeight; } try { const response = await fetch(`/api/structural/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ proposal_id: proposalId }), }); const data = await readJson(response); if (!response.ok || data.ok === false) { throw new Error(data.message || data.error || `HTTP ${response.status}`); } if (output) { const logEntry = document.createElement('div'); logEntry.className = 'log-entry log-success'; logEntry.innerHTML = `[${new Date().toLocaleTimeString()}] ✅ Proposal ${proposalId} ${action}ed`; output.appendChild(logEntry); output.scrollTop = output.scrollHeight; } refreshStatus(); loadProposals(); } catch (error) { if (output) { const logEntry = document.createElement('div'); logEntry.className = 'log-entry log-error'; logEntry.innerHTML = `[${new Date().toLocaleTimeString()}] ❌ ${action} failed: ${error.message}`; output.appendChild(logEntry); output.scrollTop = output.scrollHeight; } } } // ================================================================ // RESEARCH & DOCUMENTATION BROWSER (delegated to file-viewer.js) // ================================================================ // All file-manager logic lives in file-viewer.js and is imported above. // ================================================================ // ALPHA.5 INTEGRATION GATE STATUS // ================================================================ let gateLoaded = false; function initGateBoard() { console.log('🚪 Alpha.5 Integration Gate initializing...'); if (!gateLoaded) { refreshGateStatus(); const refreshBtn = $('gate-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', refreshGateStatus); } const closeBtn = $('gate-detail-close'); if (closeBtn) closeBtn.addEventListener('click', closeGateDetail); const detailDialog = $('gate-detail-dialog'); if (detailDialog) { detailDialog.addEventListener('click', (event) => { if (event.target === detailDialog) closeGateDetail(); }); } gateLoaded = true; } } async function refreshGateStatus() { const refreshBtn = $('gate-refresh'); if (refreshBtn) { refreshBtn.disabled = true; refreshBtn.textContent = 'Checking...'; } try { await dashboardStore.refresh(); } catch { // The store publishes its error state; keep the last truthful gate data. } renderGateBoard(dashboardStore.state); // The browser never infers scientific completion. GateStatusBuilder owns // the evidence-based result and the store is the single request path. const data = dashboardStore.state.gate; if (!data) { // Fallback: show unavailable message const overallEl = $('gate-overall'); if (overallEl) { overallEl.textContent = 'unavailable'; overallEl.className = 'gate-badge gate-failed'; } if (refreshBtn) { refreshBtn.disabled = false; refreshBtn.textContent = 'Re-check'; } return; } const scientificGate = data.scientific_gate || {}; const scientificStatus = scientificGate.overall || data.overall || 'pending'; const ciStatus = data.ci_status?.status || 'unknown'; const readiness = data.release_readiness?.overall || 'not_ready'; const scientificEl = $('gate-scientific'); if (scientificEl) { scientificEl.textContent = scientificStatus; scientificEl.className = `gate-badge gate-${scientificStatus}`; } const ciEl = $('gate-ci'); if (ciEl) { ciEl.textContent = ciStatus; ciEl.className = `gate-badge gate-${ciStatus}`; } // Overall release-readiness badge const overallEl = $('gate-overall'); if (overallEl) { overallEl.textContent = readiness; overallEl.className = `gate-badge gate-${readiness === 'ready' ? 'passed' : 'pending'}`; } // Live Runtime Profile const liveRuntime = scientificGate.live_runtime || data.live_runtime; if (liveRuntime) { renderLiveRuntime(liveRuntime); } // Gate A / B / C criteria tables const gateA = scientificGate.gate_a || data.gate_a; const gateB = scientificGate.gate_b || data.gate_b; const gateC = scientificGate.gate_c || data.gate_c; if (gateA?.items) { renderGateCriteria('gate-a-list', gateA.items); } if (gateB?.items) { renderGateCriteria('gate-b-list', gateB.items); } if (gateC?.items) { renderGateCriteria('gate-c-list', gateC.items); } if (refreshBtn) { refreshBtn.disabled = false; refreshBtn.textContent = 'Re-check'; } } // ---------------------------------------------------------------- // Render helpers for the dynamic gate // ---------------------------------------------------------------- const GATE_STATUS_ICON = { passed: '✅', pending: '⏳', blocked: '🚫', stale: '🔄', failed: '❌', }; const LIVE_STATUS_ICON = { active: '✅', disabled: '⊘', unavailable: '—', error: '❌', }; function renderLiveRuntime(items) { const container = $('gate-live-list'); if (!container) return; container.innerHTML = ''; for (const item of items) { const ls = item.live_status || 'unavailable'; const icon = LIVE_STATUS_ICON[ls] || '—'; const div = document.createElement('div'); div.className = `gate-live-item live-${ls}`; div.innerHTML = ` ${icon}
${item.name} ${item.message || ''}
`; container.appendChild(div); } } function renderGateCriteria(containerId, items) { const container = $(containerId); if (!container) return; container.innerHTML = ''; // Table header const header = document.createElement('div'); header.className = 'gate-row gate-row-header'; header.innerHTML = ` Criterion Live Maturity Gate `; container.appendChild(header); for (const item of items) { const status = item.status || 'pending'; const maturity = item.maturity || 'implemented'; const live = item.live_status || null; const icon = GATE_STATUS_ICON[status] || '…'; const liveIcon = live ? (LIVE_STATUS_ICON[live] || '—') : 'n/a'; const row = document.createElement('div'); row.className = `gate-row gate-row-${status}`; row.tabIndex = 0; row.setAttribute('role', 'button'); row.setAttribute('aria-label', `Details for ${item.label || item.id || 'criterion'}`); row.title = 'Click to view the reason and evidence'; row.innerHTML = ` ${item.label} ${item.message || ''} ${liveIcon} ${maturity.toUpperCase()} ${icon} ${status} `; row.addEventListener('click', () => openGateDetail(item)); row.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openGateDetail(item); } }); container.appendChild(row); } } function closeGateDetail() { const dialog = $('gate-detail-dialog'); if (dialog?.open) dialog.close(); } function openGateDetail(item) { const dialog = $('gate-detail-dialog'); const title = $('gate-detail-title'); const body = $('gate-detail-body'); if (!dialog || !title || !body) return; title.textContent = item.label || item.id || 'Criterion details'; body.replaceChildren(); const fields = [ ['Status', item.status || 'pending', `gate-detail-status gate-${item.status || 'pending'}`], ['Maturity', (item.maturity || 'implemented').toUpperCase(), ''], ['Live status', item.live_status || 'n/a', ''], ['Source', item.source || 'not reported', ''], ['Reason', item.message || 'No reason was reported by the backend.', 'gate-detail-reason'], ]; for (const [label, value, className] of fields) { const section = document.createElement('div'); section.className = 'gate-detail-field'; const heading = document.createElement('strong'); heading.textContent = label; const content = document.createElement('p'); content.className = className; content.textContent = String(value); section.append(heading, content); body.appendChild(section); } if (item.evidence && typeof item.evidence === 'object') { const evidence = document.createElement('div'); evidence.className = 'gate-detail-field'; const heading = document.createElement('strong'); heading.textContent = 'Evidence'; const content = document.createElement('pre'); content.textContent = JSON.stringify(item.evidence, null, 2); evidence.append(heading, content); body.appendChild(evidence); } if (!dialog.open) dialog.showModal(); } // ================================================================ // INTEGRATION STATUS MODAL (Popup für alle Versionen) // ================================================================ /** * Build version-specific integration checks. * Each version has its own set of checks that reflect what was * integrated at that stage of Brain-5D development. */ function getVersionChecks() { return { 'v0.4 (Persistenz)': [ { label: 'Delta-Journal', icon: '💾', check: async () => { try { const r = await fetch('/api/status'); return r.ok; } catch { return false; } }}, { label: 'Storage Pipeline', icon: '📦', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return d.storage?.deltas_written !== undefined; } catch { return false; } }}, { label: 'Crash Recovery', icon: '🛡️', check: async () => { try { const r = await fetch('/api/status'); return r.ok; } catch { return false; } }}, ], 'v0.5 (Integration Hardening)': [ { label: 'OperatorBridge', icon: '🌉', check: async () => { try { const r = await fetch('/api/debug/bridge'); const d = await readJson(r); return d.bridge_exists === true; } catch { return false; } }}, { label: 'Controller', icon: '🎮', check: async () => { try { const r = await fetch('/api/debug/bridge'); const d = await readJson(r); return d.controller_exists === true; } catch { return false; } }}, { label: 'Structural API', icon: '🧠', check: async () => { try { const r = await fetch('/api/structural/status'); return r.ok; } catch { return false; } }}, { label: 'Snapshots', icon: '💾', check: async () => { try { const r = await fetch('/api/snapshots'); return r.ok; } catch { return false; } }}, { label: 'Research (B5D-SEF)', icon: '🔬', check: async () => { try { const r = await fetch('/api/research'); const d = await readJson(r); return d.available === true; } catch { return false; } }}, { label: 'Heatmaps', icon: '🔥', check: async () => { try { const r = await fetch('/api/heatmap?kind=activity'); return r.ok; } catch { return false; } }}, { label: 'Docs Browser', icon: '📄', check: async () => { try { const r = await fetch('/api/docs'); return r.ok; } catch { return false; } }}, ], 'v0.5α6 (Morphological Self-Reg)': [ { label: 'Self-Organization', icon: '🧬', check: async () => { try { const r = await fetch('/api/gate/status'); const d = await readJson(r); const struct = (d.live_runtime || []).find(i => i.key === 'structural'); const s = struct?.live_status; return s === 'active' ? 'passed' : s === 'disabled' ? 'disabled' : s === 'error' ? 'failed' : s === 'unavailable' ? 'unavailable' : 'unknown'; } catch { return false; } }}, { label: 'Proposals', icon: '📋', check: async () => { try { const r = await fetch('/api/structural/proposals'); return r.ok ? 'passed' : false; } catch { return false; } }}, { label: 'Homeostasis', icon: '⚖️', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); const e = d.homeostasis?.enabled; return e === true ? 'passed' : e === false ? 'disabled' : 'unknown'; } catch { return false; } }}, ], 'v0.6 (Skalierung)': [ { label: 'Large Network', icon: '🌐', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return (d.system?.neurons || 0) > 100 ? 'passed' : 'pending'; } catch { return false; } }}, { label: 'Performance', icon: '⚡', check: async () => { try { const r = await fetch('/api/status'); return r.ok ? 'passed' : false; } catch { return false; } }}, ], 'v0.7 (Learning Env.)': [ { label: 'STDP', icon: '🧪', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return d.learning?.stdp_updates !== undefined; } catch { return false; } }}, { label: 'Reward System', icon: '🏆', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return d.learning?.reward_updates !== undefined; } catch { return false; } }}, ], 'v0.8+ (Embodiment → HMI → KI)': [ { label: 'Embodiment', icon: '🤖', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return d.embodiment !== undefined; } catch { return false; } }}, { label: 'Signal Bridge', icon: '📡', check: async () => { try { const r = await fetch('/api/status'); const d = await readJson(r); return d.signal_metrics !== undefined; } catch { return false; } }}, ], }; } /** * Open the integration status modal with all version checks. */ async function openIntegrationModal() { const modal = document.getElementById('integration-modal'); const body = document.getElementById('integration-modal-body'); if (!modal || !body) return; modal.style.display = 'flex'; body.innerHTML = ''; const versions = getVersionChecks(); let html = ''; for (const [version, checks] of Object.entries(versions)) { html += `

${escapeHtml(version)}

`; for (const c of checks) { let status = 'unknown'; let icon = '⏳'; try { const ok = await c.check(); if (ok === 'passed') { status = 'passed'; icon = '✅'; } else if (ok === 'disabled') { status = 'disabled'; icon = '⊘'; } else if (ok === 'unavailable') { status = 'unavailable'; icon = '⚠'; } else if (ok === 'pending') { status = 'pending'; icon = '⏳'; } else if (ok === 'unknown') { status = 'unknown'; icon = '?'; } else if (ok === true) { status = 'passed'; icon = '✅'; } else { status = 'failed'; icon = '❌'; } } catch { status = 'unknown'; icon = '⚠️'; } html += `
${icon} ${c.icon} ${escapeHtml(c.label)}
`; } html += '
'; } body.innerHTML = html; } /** * Close the integration modal. */ function closeIntegrationModal() { const modal = document.getElementById('integration-modal'); if (modal) modal.style.display = 'none'; } // ================================================================ // ENHANCED DASHBOARD – Interactive Features // ================================================================ /** * Make integration status items clickable – opens the modal * with detailed version-by-version integration info. */ function setupIntegrationClickHandlers() { // Click on the integration section header or badge opens the modal const header = document.querySelector('.integration-status .panel-title h2'); if (header) { header.style.cursor = 'pointer'; header.addEventListener('click', openIntegrationModal); } const badge = document.getElementById('integration-badge'); if (badge) { badge.style.cursor = 'pointer'; badge.addEventListener('click', openIntegrationModal); } // Each integration item shows a tooltip with detail on click document.querySelectorAll('.integration-item.clickable').forEach(el => { el.addEventListener('click', openIntegrationModal); }); // Modal close handlers const closeBtns = document.querySelectorAll('#modal-close-btn, #modal-close-btn2'); closeBtns.forEach(btn => btn.addEventListener('click', closeIntegrationModal)); const modal = document.getElementById('integration-modal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) closeIntegrationModal(); }); } // Refresh button const refreshBtn = document.getElementById('modal-refresh-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', openIntegrationModal); } // Escape key closes modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeIntegrationModal(); }); } /** * Add auto-refresh toggle to the dashboard. */ function setupAutoRefreshToggle() { const statusEl = document.getElementById('system-status'); if (!statusEl) return; statusEl.addEventListener('dblclick', () => { // Toggle auto-refresh on/off if (refreshInterval) { clearInterval(refreshInterval); clearInterval(heatmapInterval); refreshInterval = null; heatmapInterval = null; statusEl.textContent += ' (paused)'; } else { refreshInterval = setInterval(refreshStatus, pollInterval(1000)); heatmapInterval = setInterval(refreshHeatmap, pollInterval(5000)); statusEl.textContent = statusEl.textContent.replace(' (paused)', ''); } }); } /** * Add a system command runner to the dashboard header. */ function setupQuickActions() { // Ctrl+Shift+R = force refresh all // Ctrl+Shift+M = open integration modal // Already handled by keyboard shortcuts below } // ================================================================ // THEME & ACCESSIBILITY HEADER CONTROLS // ================================================================ const THEME_KEY = 'b5d-theme'; const ACCESSIBILITY_KEY = 'b5d-accessibility'; const CONTRAST_KEY = 'b5d-contrast'; const READER_KEY = 'b5d-reader'; function getSavedTheme() { try { return localStorage.getItem(THEME_KEY) || 'dark'; } catch { return 'dark'; } } function setTheme(theme) { document.body.dataset.theme = theme; try { localStorage.setItem(THEME_KEY, theme); } catch { // ignore storage errors } } function toggleTheme() { const current = document.body.dataset.theme || getSavedTheme(); const next = current === 'dark' ? 'light' : 'dark'; setTheme(next); } function setupThemeToggle() { const btn = $('theme-toggle'); if (!btn) return; setTheme(getSavedTheme()); btn.addEventListener('click', toggleTheme); } function setupAccessibilityToggle() { const btn = $('accessibility-toggle'); if (!btn) return; btn.addEventListener('click', () => { const enabled = document.body.classList.toggle('accessibility-mode'); try { localStorage.setItem(ACCESSIBILITY_KEY, String(enabled)); } catch { // ignore storage errors } }); try { const saved = localStorage.getItem(ACCESSIBILITY_KEY); if (saved === 'true') { document.body.classList.add('accessibility-mode'); } } catch { // ignore storage errors } } function setupReaderToggle() { const btn = $('reader-toggle'); if (!btn) return; const setReaderMode = (enabled) => { document.body.classList.toggle('reader-mode', enabled); btn.setAttribute('aria-pressed', String(enabled)); try { localStorage.setItem(READER_KEY, String(enabled)); } catch { // ignore storage errors } }; btn.addEventListener('click', () => setReaderMode(!document.body.classList.contains('reader-mode'))); try { setReaderMode(localStorage.getItem(READER_KEY) === 'true'); } catch { setReaderMode(false); } } function setupGlobalChrome() { const home = $('nav-home'); const back = $('nav-back'); const contrast = $('contrast-toggle'); const help = $('help-toggle'); const helpDialog = $('context-help'); const helpCopy = { overview: ['System Overview', 'Verdichtet Runtime, Health, wissenschaftlichen Status und die wichtigsten Messwerte.'], network: ['Network Workbench', 'Untersucht reale Live-Dynamik, Projektionen, Populationen, Neuronen und Synapsen. Auflösungsregler steuern die Backend-Aggregation.'], control: ['Control Workbench', 'Steuert Ticks, Loop, Snapshot, Experiment-Sessions und freigabepflichtige Strukturänderungen.'], research: ['Research Workspace', 'Durchsucht Registry, Experimente, Evidenz, Reports und Dokumentation aus dem Repository.'], gate: ['Release Workspace', 'Trennt Scientific Gate, Source-CI und Release Readiness. Stale bedeutet: Source wurde seit der Evidenz verändert.'], settings: ['Scientific Settings', 'Änderungen werden zunächst als Pending Change vorgemerkt. S markiert wissenschaftlich sensitive Werte, R einen notwendigen Neustart.'], embodiment: ['Embodiment Workspace', 'Zeigt ausschließlich publizierte Sensor-, Aktuator-, Episoden- und Reward-Metriken. Unconfigured ist kein simulierter Zustand.'], }; home?.addEventListener('click', () => document.querySelector('.tab-btn[data-tab="overview"]')?.click()); back?.addEventListener('click', () => { const target = document.body.dataset.previousTab || 'overview'; document.querySelector(`.tab-btn[data-tab="${target}"]`)?.click(); }); contrast?.addEventListener('click', () => { const enabled = document.body.classList.toggle('contrast-mode'); localStorage.setItem(CONTRAST_KEY, String(enabled)); }); if (localStorage.getItem(CONTRAST_KEY) === 'true') document.body.classList.add('contrast-mode'); help?.addEventListener('click', () => { const [title, text] = helpCopy[document.body.dataset.currentTab || 'overview']; setText('context-help-title', title); setText('context-help-text', text); helpDialog?.showModal(); }); $('context-help-close')?.addEventListener('click', () => helpDialog?.close()); } // ================================================================ // KEYBOARD SHORTCUTS (Global) // ================================================================ function setupGlobalShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl+R = Refresh (if not on docs tab) if (e.ctrlKey && e.key === 'r') { e.preventDefault(); refreshStatus(); refreshHeatmap(); return; } // Ctrl+Shift+I = Open Integration Modal if (e.ctrlKey && e.shiftKey && e.key === 'I') { e.preventDefault(); openIntegrationModal(); return; } // Ctrl+1-8 = Tab switching if (e.ctrlKey && e.key >= '1' && e.key <= '8') { e.preventDefault(); const index = parseInt(e.key) - 1; const tabs = $$('.tab-btn'); if (tabs[index]) { tabs[index].click(); } return; } }); } // ================================================================ // INITIALIZE APPLICATION // ================================================================ function init() { console.log('🧠 Brain-5D Operator Dashboard v3.0.0'); sharedExperimentMode = new ExperimentMode(); sharedExperimentMode.refresh(); // Setup tab navigation (also initializes components lazily) setupTabs(); setupOverviewActions(); setupWorkspaceViews(); setupResearchLanes(); initBoxStates(); initEmbodimentDetails(); initEmbodimentPipelineControls(); // Initialize dashboard (status & heatmap) initDashboard(); // Setup integration click handlers setupIntegrationClickHandlers(); // Setup auto-refresh toggle setupAutoRefreshToggle(); // Setup global shortcuts setupGlobalShortcuts(); // Setup header controls setupThemeToggle(); setupAccessibilityToggle(); setupReaderToggle(); setupGlobalChrome(); console.log('✅ Dashboard ready'); } // ================================================================ // START // ================================================================ // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }