diff --git a/main.py b/main.py index 6ffe363..6d3efb3 100644 --- a/main.py +++ b/main.py @@ -142,7 +142,7 @@ class ArmorStationStack(QStackedWidget): class ArmorOS(QWidget): - """ARMOR window with a persistent laptop prompt bar.""" + """Single-purpose Brain Node shell on the laptop profile.""" def __init__(self): super().__init__() @@ -151,9 +151,12 @@ class ArmorOS(QWidget): get_window_title() ) + self.prompt_bar = None + self.shire_page = None + if ARMOR_DISPLAY_PROFILE == "laptop": self.setWindowTitle( - "ARMOR OS - Laptop Command Station" + "SHiRE Brain Node 01" ) self.setWindowFlag( @@ -161,61 +164,112 @@ class ArmorOS(QWidget): True, ) - self.stack = ArmorStationStack(self) + self.setFocusPolicy(Qt.NoFocus) + self.setCursor(Qt.BlankCursor) + + self.stack = QStackedWidget(self) + self.stack.setFocusPolicy(Qt.NoFocus) - if ARMOR_DISPLAY_PROFILE == "laptop": home_page = LaptopDashboard( self.stack ) + + self.stack.addWidget(home_page) + self.stack.setCurrentIndex(0) + else: + self.stack = ArmorStationStack(self) + home_page = Dashboard( self.stack ) - self.shire_page = ShirePage( - self.stack - ) + self.shire_page = ShirePage( + self.stack + ) - pages = [ - home_page, - self.shire_page, - ForgePage(self.stack), - ArchivePage(self.stack), - AtlasPage(self.stack), - MediaPage(self.stack), - SystemPage(self.stack), - AgentsPage(self.stack), - TasksPage(self.stack), - SettingsPage(self.stack), - SentinelPage(self.stack), - EducationPage(self.stack), - ] - - for page in pages: - self.stack.addWidget(page) + pages = [ + home_page, + self.shire_page, + ForgePage(self.stack), + ArchivePage(self.stack), + AtlasPage(self.stack), + MediaPage(self.stack), + SystemPage(self.stack), + AgentsPage(self.stack), + TasksPage(self.stack), + SettingsPage(self.stack), + SentinelPage(self.stack), + EducationPage(self.stack), + ] + + for page in pages: + self.stack.addWidget(page) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - self.prompt_bar = None - - if ARMOR_DISPLAY_PROFILE == "laptop": - self.prompt_bar = GlobalPromptBar( - self.stack, - self.shire_page, - self, - ) - - layout.addWidget( - self.prompt_bar - ) - layout.addWidget( self.stack, 1, ) + def keyPressEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().keyPressEvent(event) + + def mousePressEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().mouseReleaseEvent(event) + + def mouseDoubleClickEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().mouseDoubleClickEvent(event) + + def wheelEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().wheelEvent(event) + + def contextMenuEvent(self, event): + if ARMOR_DISPLAY_PROFILE == "laptop": + event.accept() + return + + super().contextMenuEvent(event) + + def closeEvent(self, event): + if ( + ARMOR_DISPLAY_PROFILE == "laptop" + and os.environ.get( + "SHIRE_ALLOW_LOCAL_CLOSE", + "", + ) != "1" + ): + event.ignore() + return + + super().closeEvent(event) + def main(): app = QApplication(sys.argv) diff --git a/modules/laptop_dashboard.py b/modules/laptop_dashboard.py index d5d0384..009336b 100644 --- a/modules/laptop_dashboard.py +++ b/modules/laptop_dashboard.py @@ -1,8 +1,18 @@ from datetime import datetime +import json import math from pathlib import Path - -from PyQt5.QtCore import Qt, QRectF, QTimer +import subprocess +import urllib.error +import urllib.request + +from PyQt5.QtCore import ( + Qt, + QRectF, + QThread, + QTimer, + pyqtSignal, +) from PyQt5.QtGui import ( QBrush, QColor, @@ -15,6 +25,7 @@ from PyQt5.QtGui import ( QRadialGradient, ) from PyQt5.QtWidgets import ( + QApplication, QFrame, QGridLayout, QHBoxLayout, @@ -785,6 +796,1160 @@ class ShireCenterCore(QWidget): ) + +def _brain_node_base_url(): + """Resolve the local Brain Node API on its Tailnet-only listener.""" + try: + result = subprocess.run( + ["tailscale", "ip", "-4"], + check=False, + capture_output=True, + text=True, + timeout=4, + ) + except Exception: + return "" + + for line in result.stdout.splitlines(): + address = line.strip() + + if address: + return f"http://{address}:8765" + + return "" + + +def _brain_node_json(url, timeout): + request = urllib.request.Request(url, method="GET") + + with urllib.request.urlopen( + request, + timeout=timeout, + ) as response: + return json.loads( + response.read().decode("utf-8") + ) + + +def fetch_brain_node_snapshot(): + """Fetch truthful worker state without assigning it authority.""" + base_url = _brain_node_base_url() + + if not base_url: + return { + "reachable": False, + "error": "Tailnet address unavailable", + } + + try: + heartbeat = _brain_node_json( + base_url + "/worker/heartbeat", + 4, + ) + capabilities = _brain_node_json( + base_url + "/worker/capabilities", + 12, + ) + + return { + "reachable": True, + "base_url": base_url, + "heartbeat": heartbeat, + "capabilities": capabilities, + "error": None, + } + + except ( + OSError, + ValueError, + urllib.error.HTTPError, + urllib.error.URLError, + ) as exc: + return { + "reachable": False, + "base_url": base_url, + "error": f"{type(exc).__name__}: {exc}", + } + + +class BrainNodeProbe(QThread): + """Non-blocking heartbeat probe for the laptop dashboard.""" + + completed = pyqtSignal(dict) + + def run(self): + self.completed.emit(fetch_brain_node_snapshot()) + + +class BrainNodeVisual(QWidget): + """Animated electrical Brain Node driven by real worker state.""" + + def __init__(self, hero=False): + super().__init__() + + self.hero_mode = bool(hero) + self.phase = 0.0 + self.node_state = "CONNECTING" + self.state_detail = "WAITING FOR WORKER HEARTBEAT" + self.capability_detail = "CAPABILITIES UNKNOWN" + self.model_detail = "DEEP MODEL UNKNOWN" + self.authority_detail = "COMMAND CORE AUTHORITY CHECK PENDING" + self.sequence = 0 + self.probe = None + self._shutting_down = False + + self.setMinimumHeight( + 470 if self.hero_mode else 255 + ) + self.setSizePolicy( + QSizePolicy.Expanding, + QSizePolicy.Expanding, + ) + + self.animation_timer = QTimer(self) + self.animation_timer.setTimerType(Qt.PreciseTimer) + self.animation_timer.timeout.connect( + self._animate + ) + self.animation_timer.start(80) + + self.poll_timer = QTimer(self) + self.poll_timer.timeout.connect( + self.refresh_heartbeat + ) + self.poll_timer.start(3000) + + app = QApplication.instance() + + if app is not None: + app.aboutToQuit.connect(self.shutdown) + + QTimer.singleShot( + 50, + self.refresh_heartbeat, + ) + + def _animate(self): + speed = 0.018 + + if self.node_state == "ONLINE": + speed = 0.075 + elif self.node_state == "DEGRADED": + speed = 0.038 + elif self.node_state in {"OFFLINE", "AUTHORITY FAULT"}: + speed = 0.006 + + self.phase = ( + self.phase + speed + ) % (math.pi * 2000.0) + + self.update() + + def refresh_heartbeat(self): + if self._shutting_down: + return + + if self.probe is not None: + return + + probe = BrainNodeProbe(self) + self.probe = probe + + probe.completed.connect( + self.apply_snapshot + ) + + probe.finished.connect( + lambda probe=probe: self._probe_finished( + probe + ) + ) + + probe.start() + + def _probe_finished(self, probe): + if self.probe is probe: + self.probe = None + + probe.deleteLater() + + def shutdown(self): + if self._shutting_down: + return + + self._shutting_down = True + + if self.animation_timer.isActive(): + self.animation_timer.stop() + + if self.poll_timer.isActive(): + self.poll_timer.stop() + + probe = self.probe + + if probe is None: + return + + try: + probe.completed.disconnect( + self.apply_snapshot + ) + except (TypeError, RuntimeError): + pass + + probe.requestInterruption() + + if probe.isRunning(): + probe.wait(15000) + + if self.probe is probe: + self.probe = None + + probe.deleteLater() + + def apply_snapshot(self, snapshot): + if not snapshot.get("reachable"): + self.node_state = "OFFLINE" + self.state_detail = "WORKER HEARTBEAT UNAVAILABLE" + self.capability_detail = "LOCAL DISPLAY ONLY" + self.model_detail = "NO REMOTE COMPUTE CONNECTION" + self.authority_detail = "NO AUTHORITY TRANSFER" + self.update() + return + + heartbeat = snapshot.get("heartbeat", {}) + capability_payload = snapshot.get( + "capabilities", + {}, + ) + + reported_state = str( + heartbeat.get("state", "degraded") + ).strip().upper() + + if reported_state not in { + "ONLINE", + "DEGRADED", + }: + reported_state = "DEGRADED" + + ownership = capability_payload.get( + "ownership", + heartbeat.get("ownership", {}), + ) + + prohibited_authority = ( + "owns_projects", + "owns_approvals", + "owns_memory", + "owns_shirevault", + "makes_safety_decisions", + ) + + authority_safe = all( + ownership.get(name) is False + for name in prohibited_authority + ) + + if not authority_safe: + self.node_state = "AUTHORITY FAULT" + self.authority_detail = ( + "WORKER AUTHORITY BOUNDARY VIOLATION" + ) + else: + self.node_state = reported_state + self.authority_detail = ( + "MINI OWNS PROJECTS • APPROVALS • SAFETY" + ) + + self.sequence = int( + heartbeat.get("sequence", 0) + ) + + summary = capability_payload.get( + "summary", + heartbeat.get("capability_summary", {}), + ) + + available = int(summary.get("available", 0)) + registered = int(summary.get("registered", 0)) + + self.capability_detail = ( + f"{available}/{registered} CAPABILITIES AVAILABLE" + ) + + deep_model = "DEEP MODEL UNKNOWN" + + for capability in capability_payload.get( + "capabilities", + [], + ): + if capability.get("id") == "deep_reasoning": + dependencies = capability.get( + "dependencies", + [], + ) + + if dependencies: + deep_model = str(dependencies[0]) + + break + + self.model_detail = deep_model.upper() + + uptime = heartbeat.get("uptime_seconds", 0) + + self.state_detail = ( + f"HEARTBEAT {self.sequence} • " + f"UPTIME {int(float(uptime))}S" + ) + + self.update() + + def showEvent(self, event): + super().showEvent(event) + + if self._shutting_down: + return + + if not self.animation_timer.isActive(): + self.animation_timer.start(80) + + if not self.poll_timer.isActive(): + self.poll_timer.start(3000) + + self.refresh_heartbeat() + + def hideEvent(self, event): + if self.animation_timer.isActive(): + self.animation_timer.stop() + + if self.poll_timer.isActive(): + self.poll_timer.stop() + + super().hideEvent(event) + + def _state_color(self): + if self.node_state == "ONLINE": + return QColor(GREEN) + + if self.node_state == "DEGRADED": + return QColor("#ffbd4a") + + if self.node_state == "AUTHORITY FAULT": + return QColor("#ff4265") + + if self.node_state == "CONNECTING": + return QColor(CYAN) + + return QColor("#7f728f") + + @staticmethod + def _brain_paths(cx, cy, scale): + left = QPainterPath() + left.moveTo(cx - 2 * scale, cy - 68 * scale) + left.cubicTo( + cx - 34 * scale, + cy - 84 * scale, + cx - 65 * scale, + cy - 66 * scale, + cx - 62 * scale, + cy - 38 * scale, + ) + left.cubicTo( + cx - 82 * scale, + cy - 25 * scale, + cx - 76 * scale, + cy + 6 * scale, + cx - 58 * scale, + cy + 15 * scale, + ) + left.cubicTo( + cx - 70 * scale, + cy + 43 * scale, + cx - 48 * scale, + cy + 69 * scale, + cx - 25 * scale, + cy + 60 * scale, + ) + left.cubicTo( + cx - 13 * scale, + cy + 73 * scale, + cx - 2 * scale, + cy + 55 * scale, + cx - 2 * scale, + cy + 31 * scale, + ) + left.closeSubpath() + + right = QPainterPath() + right.moveTo(cx + 2 * scale, cy - 68 * scale) + right.cubicTo( + cx + 34 * scale, + cy - 84 * scale, + cx + 65 * scale, + cy - 66 * scale, + cx + 62 * scale, + cy - 38 * scale, + ) + right.cubicTo( + cx + 82 * scale, + cy - 25 * scale, + cx + 76 * scale, + cy + 6 * scale, + cx + 58 * scale, + cy + 15 * scale, + ) + right.cubicTo( + cx + 70 * scale, + cy + 43 * scale, + cx + 48 * scale, + cy + 69 * scale, + cx + 25 * scale, + cy + 60 * scale, + ) + right.cubicTo( + cx + 13 * scale, + cy + 73 * scale, + cx + 2 * scale, + cy + 55 * scale, + cx + 2 * scale, + cy + 31 * scale, + ) + right.closeSubpath() + + return left, right + + def _draw_electricity( + self, + painter, + cx, + cy, + radius_x, + radius_y, + color, + ): + if self.node_state not in { + "ONLINE", + "DEGRADED", + }: + return + + arc_count = ( + 8 if self.node_state == "ONLINE" else 4 + ) + + for arc_index in range(arc_count): + start = ( + self.phase + + arc_index + * (math.pi * 2.0 / arc_count) + ) + + path = QPainterPath() + + for point_index in range(9): + angle = start + point_index * 0.105 + jitter = ( + math.sin( + self.phase * 5.0 + + arc_index * 2.3 + + point_index * 3.1 + ) + * 6.0 + ) + + x = ( + cx + + math.cos(angle) + * (radius_x + jitter) + ) + y = ( + cy + + math.sin(angle) + * (radius_y + jitter * 0.45) + ) + + if point_index == 0: + path.moveTo(x, y) + else: + path.lineTo(x, y) + + pulse = ( + math.sin( + self.phase * 7.0 + + arc_index + ) + + 1.0 + ) / 2.0 + + electric = QColor(color) + electric.setAlpha( + int(95 + pulse * 155) + ) + + painter.setPen( + QPen( + electric, + 1.2 + pulse * 1.8, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, + ) + ) + painter.setBrush(Qt.NoBrush) + painter.drawPath(path) + + def _draw_hero_lightning( + self, + painter, + cx, + cy, + radius_x, + radius_y, + state_color, + ): + if not self.hero_mode: + return + + if self.node_state not in { + "ONLINE", + "DEGRADED", + }: + return + + online = self.node_state == "ONLINE" + bolt_count = 18 if online else 9 + + flash = 0.0 + + if online: + flash = max( + 0.0, + math.sin(self.phase * 1.72), + ) ** 16 + + for ring_index in range(5): + ring_pulse = ( + math.sin( + self.phase * 2.4 + + ring_index * 1.05 + ) + + 1.0 + ) / 2.0 + + ring_scale = ( + 0.80 + + ring_index * 0.14 + + ring_pulse * 0.03 + ) + + ring_color = QColor(state_color) + ring_color.setAlpha( + int(20 + ring_pulse * 70) + ) + + painter.setPen( + QPen( + ring_color, + 1.0 + ring_pulse * 1.5, + ) + ) + painter.setBrush(Qt.NoBrush) + + painter.drawEllipse( + QRectF( + cx - radius_x * ring_scale, + cy - radius_y * ring_scale, + radius_x * ring_scale * 2.0, + radius_y * ring_scale * 2.0, + ) + ) + + for bolt_index in range(bolt_count): + angle = ( + self.phase * 0.31 + + bolt_index + * (math.pi * 2.0 / bolt_count) + ) + + surge = ( + math.sin( + self.phase * 6.5 + + bolt_index * 1.43 + ) + + 1.0 + ) / 2.0 + + inner_x = radius_x * ( + 0.62 + surge * 0.04 + ) + inner_y = radius_y * ( + 0.62 + surge * 0.04 + ) + + outer_multiplier = ( + 1.24 + + surge * 0.34 + + flash * 0.36 + ) + + outer_x = radius_x * outer_multiplier + outer_y = radius_y * outer_multiplier + + path = QPainterPath() + segments = 8 + + for segment in range(segments + 1): + progress = segment / segments + + radial_x = ( + inner_x + + (outer_x - inner_x) * progress + ) + radial_y = ( + inner_y + + (outer_y - inner_y) * progress + ) + + zigzag = ( + math.sin( + segment * 5.1 + + bolt_index * 2.8 + + self.phase * 12.0 + ) + * 13.0 + * math.sin(math.pi * progress) + ) + + x = ( + cx + + math.cos(angle) * radial_x + + math.cos( + angle + math.pi / 2.0 + ) * zigzag + ) + + y = ( + cy + + math.sin(angle) * radial_y + + math.sin( + angle + math.pi / 2.0 + ) * zigzag + ) + + if segment == 0: + path.moveTo(x, y) + else: + path.lineTo(x, y) + + alpha = min( + 255, + int( + 80 + + surge * 145 + + flash * 110 + ), + ) + + glow = QColor(state_color) + glow.setAlpha(max(20, alpha // 4)) + + painter.setPen( + QPen( + glow, + 9.0, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, + ) + ) + painter.drawPath(path) + + bolt = QColor(state_color) + bolt.setAlpha(alpha) + + painter.setPen( + QPen( + bolt, + 1.5 + surge * 2.0, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, + ) + ) + painter.drawPath(path) + + if flash > 0.15 and bolt_index % 3 == 0: + white_core = QColor( + 235, + 250, + 255, + int(90 + flash * 165), + ) + + painter.setPen( + QPen( + white_core, + 0.9 + flash * 1.2, + Qt.SolidLine, + Qt.RoundCap, + Qt.RoundJoin, + ) + ) + painter.drawPath(path) + + def paintEvent(self, event): + del event + + painter = QPainter(self) + painter.setRenderHint( + QPainter.Antialiasing, + True, + ) + + w = float(self.width()) + h = float(self.height()) + cx = w / 2.0 + cy = h * ( + 0.43 if self.hero_mode else 0.40 + ) + + if self.hero_mode: + scale = max( + 0.90, + min(w / 240.0, h / 320.0), + ) + else: + scale = max( + 0.65, + min(w / 250.0, h / 320.0), + ) + + state_color = self._state_color() + + flash = 0.0 + + if ( + self.hero_mode + and self.node_state == "ONLINE" + ): + flash = max( + 0.0, + math.sin(self.phase * 1.72), + ) ** 16 + + if flash > 0.001: + flash_aura = QRadialGradient( + cx, + cy, + 190.0 * scale, + ) + + flash_aura.setColorAt( + 0.0, + QColor( + 235, + 248, + 255, + int(65 + flash * 145), + ), + ) + flash_aura.setColorAt( + 0.22, + QColor( + 90, + 135, + 255, + int(45 + flash * 115), + ), + ) + flash_aura.setColorAt( + 0.62, + QColor( + 155, + 65, + 255, + int(20 + flash * 75), + ), + ) + flash_aura.setColorAt( + 1.0, + QColor(0, 0, 0, 0), + ) + + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(flash_aura)) + + painter.drawEllipse( + QRectF( + cx - 190 * scale, + cy - 165 * scale, + 380 * scale, + 330 * scale, + ) + ) + + pulse = ( + math.sin(self.phase * 2.4) + 1.0 + ) / 2.0 + + aura = QRadialGradient( + cx, + cy, + 118.0 * scale, + ) + + center_color = QColor(state_color) + center_color.setAlpha( + int(45 + pulse * 55) + ) + + aura.setColorAt(0.0, center_color) + aura.setColorAt( + 0.55, + QColor(60, 15, 105, 40), + ) + aura.setColorAt( + 1.0, + QColor(0, 0, 0, 0), + ) + + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(aura)) + painter.drawEllipse( + QRectF( + cx - 118 * scale, + cy - 105 * scale, + 236 * scale, + 210 * scale, + ) + ) + + electricity_radius_x = ( + 122 if self.hero_mode else 93 + ) * scale + + electricity_radius_y = ( + 96 if self.hero_mode else 75 + ) * scale + + self._draw_electricity( + painter, + cx, + cy, + electricity_radius_x, + electricity_radius_y, + state_color, + ) + + self._draw_hero_lightning( + painter, + cx, + cy, + electricity_radius_x, + electricity_radius_y, + state_color, + ) + + left, right = self._brain_paths( + cx, + cy, + scale, + ) + + brain_fill = QRadialGradient( + cx, + cy - 12 * scale, + 92 * scale, + ) + brain_fill.setColorAt( + 0.0, + QColor(75, 30, 125, 245), + ) + brain_fill.setColorAt( + 0.52, + QColor(36, 12, 73, 245), + ) + brain_fill.setColorAt( + 1.0, + QColor(10, 5, 28, 250), + ) + + outline = QColor(state_color) + outline.setAlpha( + int(185 + pulse * 65) + ) + + painter.setPen( + QPen(outline, 2.3) + ) + painter.setBrush(QBrush(brain_fill)) + painter.drawPath(left) + painter.drawPath(right) + + core_pulse = ( + math.sin(self.phase * 5.2) + 1.0 + ) / 2.0 + + core_radius = ( + 7.0 + + core_pulse * 4.5 + + flash * 10.0 + ) * scale + + core_glow = QRadialGradient( + cx, + cy + 3 * scale, + core_radius * 3.8, + ) + + core_glow.setColorAt( + 0.0, + QColor( + 245, + 252, + 255, + int(205 + flash * 50), + ), + ) + + core_glow.setColorAt( + 0.28, + QColor( + state_color.red(), + state_color.green(), + state_color.blue(), + int(165 + core_pulse * 85), + ), + ) + + core_glow.setColorAt( + 1.0, + QColor(0, 0, 0, 0), + ) + + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(core_glow)) + + painter.drawEllipse( + QRectF( + cx - core_radius * 3.8, + cy + 3 * scale - core_radius * 3.8, + core_radius * 7.6, + core_radius * 7.6, + ) + ) + + painter.setPen( + QPen( + QColor( + 240, + 250, + 255, + 235, + ), + 1.5, + ) + ) + painter.setBrush(state_color) + + painter.drawEllipse( + QRectF( + cx - core_radius, + cy + 3 * scale - core_radius, + core_radius * 2.0, + core_radius * 2.0, + ) + ) + + groove_pen = QColor(PURPLE_LIGHT) + groove_pen.setAlpha(150) + + painter.setPen( + QPen( + groove_pen, + 1.35, + Qt.SolidLine, + Qt.RoundCap, + ) + ) + painter.setBrush(Qt.NoBrush) + + grooves = ( + (-47, -44, -21, -29, -42, -9), + (-58, -15, -28, -4, -45, 18), + (-47, 23, -22, 19, -32, 47), + (-24, -58, -11, -37, -24, -14), + (47, -44, 21, -29, 42, -9), + (58, -15, 28, -4, 45, 18), + (47, 23, 22, 19, 32, 47), + (24, -58, 11, -37, 24, -14), + ) + + for x1, y1, cx1, cy1, x2, y2 in grooves: + path = QPainterPath() + path.moveTo( + cx + x1 * scale, + cy + y1 * scale, + ) + path.quadTo( + cx + cx1 * scale, + cy + cy1 * scale, + cx + x2 * scale, + cy + y2 * scale, + ) + painter.drawPath(path) + + neural_points = ( + (-42, -32), + (-20, -46), + (-35, 2), + (-16, 25), + (-31, 43), + (42, -32), + (20, -46), + (35, 2), + (16, 25), + (31, 43), + (0, -18), + (0, 19), + ) + + neural_links = ( + (0, 1), + (0, 2), + (1, 10), + (2, 3), + (3, 4), + (3, 11), + (5, 6), + (5, 7), + (6, 10), + (7, 8), + (8, 9), + (8, 11), + (10, 11), + ) + + for link_index, (first, second) in enumerate( + neural_links + ): + x1, y1 = neural_points[first] + x2, y2 = neural_points[second] + + glow = ( + math.sin( + self.phase * 4.5 + + link_index * 0.75 + ) + + 1.0 + ) / 2.0 + + link_color = QColor(state_color) + link_color.setAlpha( + int(55 + glow * 145) + ) + + painter.setPen( + QPen(link_color, 1.1) + ) + painter.drawLine( + int(cx + x1 * scale), + int(cy + y1 * scale), + int(cx + x2 * scale), + int(cy + y2 * scale), + ) + + for point_index, (x, y) in enumerate( + neural_points + ): + node_pulse = ( + math.sin( + self.phase * 5.5 + + point_index * 0.9 + ) + + 1.0 + ) / 2.0 + + node_color = QColor(state_color) + node_color.setAlpha( + int(145 + node_pulse * 110) + ) + + radius = ( + 2.0 + node_pulse * 1.8 + ) * scale + + painter.setPen(Qt.NoPen) + painter.setBrush(node_color) + painter.drawEllipse( + QRectF( + cx + x * scale - radius, + cy + y * scale - radius, + radius * 2, + radius * 2, + ) + ) + + label_top = h - 82 + + painter.setPen(state_color) + painter.setFont( + QFont( + "DejaVu Sans Mono", + 13, + QFont.Bold, + ) + ) + painter.drawText( + QRectF(0, label_top, w, 20), + Qt.AlignCenter, + f"SHIRE BRAIN NODE 01 • {self.node_state}", + ) + + painter.setPen(QColor(PURPLE_LIGHT)) + painter.setFont( + QFont( + "DejaVu Sans Mono", + 8, + QFont.Bold, + ) + ) + painter.drawText( + QRectF(0, label_top + 21, w, 15), + Qt.AlignCenter, + self.state_detail, + ) + + painter.setPen(QColor(CYAN)) + painter.drawText( + QRectF(0, label_top + 37, w, 15), + Qt.AlignCenter, + self.capability_detail, + ) + + painter.setPen(QColor(MUTED)) + painter.drawText( + QRectF(0, label_top + 53, w, 15), + Qt.AlignCenter, + self.model_detail, + ) + + authority_color = ( + QColor(GREEN) + if self.node_state != "AUTHORITY FAULT" + else QColor("#ff4265") + ) + + painter.setPen(authority_color) + painter.drawText( + QRectF(0, label_top + 68, w, 14), + Qt.AlignCenter, + self.authority_detail, + ) + + class VoiceOrb(QWidget): """Visual mirror of the approved manual voice state.""" @@ -1035,6 +2200,20 @@ class LaptopDashboard(QWidget): return card + def _build_brain_node_card(self): + card = PresenceCard( + "DISTRIBUTED BRAIN NODE", + CYAN, + ) + + self.brain_node_visual = BrainNodeVisual() + card.column.addWidget( + self.brain_node_visual, + 1, + ) + + return card + def _build_thought_card(self): card = PresenceCard("CURRENT THOUGHT", PURPLE_LIGHT) @@ -1144,155 +2323,45 @@ class LaptopDashboard(QWidget): return drawer def build_ui(self): - self.setObjectName("presenceConsole") + """Build a display-only Brain Node kiosk.""" + + self.setObjectName("brainNodeKiosk") + self.setFocusPolicy(Qt.NoFocus) + self.setAttribute( + Qt.WA_TransparentForMouseEvents, + True, + ) self.setStyleSheet(""" - QWidget#presenceConsole { - background: #020208; - color: #f4efff; - font-family: "DejaVu Sans Mono", monospace; - } - QLabel { - background: transparent; + QWidget#brainNodeKiosk { + background: #000000; + border: none; } """) root = QVBoxLayout(self) - root.setContentsMargins(26, 20, 26, 16) - root.setSpacing(12) - - header = QHBoxLayout() - header.setSpacing(18) - - brand_column = QVBoxLayout() - brand_column.setSpacing(0) - - brand = QLabel("SHIRE AIOS") - brand.setStyleSheet( - f"color:{PURPLE}; " - "font-size:27px; " - "font-weight:900; " - "letter-spacing:2px;" - ) - - core_state = QLabel("CORE ONLINE") - core_state.setStyleSheet( - f"color:{GREEN}; " - "font-size:10px; " - "font-weight:900;" - ) - - brand_column.addWidget(brand) - brand_column.addWidget(core_state) - - header.addLayout(brand_column) - header.addStretch(1) - header.addWidget(VoicePrivacyBanner()) - - root.addLayout(header) - - stage = QHBoxLayout() - stage.setSpacing(14) - - left_column = QVBoxLayout() - left_column.setSpacing(12) - left_column.addWidget(self._build_greeting_card(), 3) - left_column.addWidget(self._build_status_card(), 2) - - center_column = QVBoxLayout() - center_column.setSpacing(2) - - center_title = QLabel("SHIRE PRESENCE") - center_title.setAlignment(Qt.AlignCenter) - center_title.setStyleSheet( - f"color:{PURPLE_LIGHT}; " - "font-size:12px; " - "font-weight:900; " - "letter-spacing:2px;" - ) - - center_column.addWidget(center_title) - center_column.addWidget(ShireCenterCore(), 1) - center_column.addWidget(VoiceOrb()) - - right_column = QVBoxLayout() - right_column.setSpacing(12) - right_column.addWidget(self._build_thought_card(), 3) - right_column.addWidget(self._build_next_card(), 2) - - stage.addLayout(left_column, 5) - stage.addLayout(center_column, 8) - stage.addLayout(right_column, 5) - - root.addLayout(stage, 1) - - dock = QHBoxLayout() - dock.setSpacing(10) + root.setContentsMargins(0, 0, 0, 0) + root.setSpacing(0) - dock.addWidget( - self._make_nav_button("●", "SHIRE", 1) - ) - - dock.addWidget( - self._make_nav_button("⚒", "FORGE", 2) + self.station_drawer = None + self.brain_node_visual = BrainNodeVisual( + hero=True ) - dock.addWidget( - self._make_nav_button("♜", "SENTINEL", 10) + self.brain_node_visual.setFocusPolicy( + Qt.NoFocus ) - stations_button = QPushButton("☰ STATIONS") - stations_button.setCursor(Qt.PointingHandCursor) - stations_button.setMinimumHeight(48) - - stations_button.setStyleSheet(""" - QPushButton { - background: rgba(8, 5, 25, 225); - color: #00f58a; - border: 1px solid rgba(0, 245, 138, 185); - border-radius: 13px; - font-family: "DejaVu Sans Mono", monospace; - font-size: 13px; - font-weight: 900; - padding: 8px 12px; - } - QPushButton:hover { - background: rgba(0, 55, 39, 230); - color: #ffffff; - border: 1px solid #00f58a; - } - QPushButton:pressed { - background: rgba(155, 77, 255, 75); - } - """) - - stations_button.clicked.connect( - self._toggle_stations - ) - - dock.addWidget(stations_button) - - root.addLayout(dock) - - self.station_drawer = self._build_station_drawer() - root.addWidget(self.station_drawer) - - hint = QLabel( - "VOICE PREVIEW READY • " - "Use MIC in the top prompt bar • " - "Audio is processed locally and discarded" + self.brain_node_visual.setAttribute( + Qt.WA_TransparentForMouseEvents, + True, ) - hint.setAlignment(Qt.AlignCenter) - hint.setStyleSheet( - "color:#8f82a8; " - "font-size:9px; " - "font-weight:800; " - "padding:1px;" + root.addWidget( + self.brain_node_visual, + 1, ) - root.addWidget(hint) - def paintEvent(self, event): del event