Generated UTC: 2026-08-02T10:02:22+00:00 Host: Work Pixel ADB device: 4B291FDKD000HK Sentinel SHA-256: 4fa53399f013e76ab05b3a4f093a1a7a4d5b48279a5e7a2a336a159a9c271d11 ============================================================ PIXEL DEVICE ============================================================ Google Pixel 9 Pro Fold 17 37 google/comet/comet:17/CP2A.260705.006/15641320:user/release-keys ============================================================ PIXEL NFC / CAMERA / MICROPHONE FEATURES ============================================================ feature:android.hardware.bluetooth feature:android.hardware.bluetooth_le feature:android.hardware.bluetooth_le.channel_sounding feature:android.hardware.camera feature:android.hardware.camera.any feature:android.hardware.camera.autofocus feature:android.hardware.camera.capability.manual_post_processing feature:android.hardware.camera.capability.manual_sensor feature:android.hardware.camera.capability.raw feature:android.hardware.camera.concurrent feature:android.hardware.camera.flash feature:android.hardware.camera.front feature:android.hardware.camera.level.full feature:android.hardware.microphone feature:android.hardware.nfc feature:android.hardware.nfc.any feature:android.hardware.nfc.ese feature:android.hardware.nfc.hce feature:android.hardware.nfc.hcef feature:android.hardware.wifi feature:android.hardware.wifi.aware feature:android.hardware.wifi.direct feature:android.hardware.wifi.passpoint feature:android.hardware.wifi.rtt feature:com.google.pixel.camera.concurrent_foldable_dual_front feature:vendor.android.hardware.camera.preview-dis.back feature:vendor.android.hardware.camera.preview-dis.front ============================================================ INSTALLED SHIRE MOBILE ============================================================ package:/data/app/~~Ccn0w3QqNFA8hjrM4PA0tQ==/com.shire.mobile-I-W9RRMiz84k5OIrQjuKkA==/base.apk versionCode=7 minSdk=29 targetSdk=36 versionName=0.6.1-private-approval-inbox-startup-fix lastUpdateTime=2026-08-02 17:20:46 android.permission.POST_NOTIFICATIONS android.permission.INTERNET android.permission.ACCESS_NETWORK_STATE android.permission.RECEIVE_BOOT_COMPLETED: granted=true android.permission.INTERNET: granted=true android.permission.ACCESS_NETWORK_STATE: granted=true com.shire.mobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION: granted=true firstInstallTime=2026-08-02 09:28:15 android.permission.POST_NOTIFICATIONS: granted=true, flags=[ USER_SET|USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED] android.permission.ACCESS_LOCAL_NETWORK: granted=true, flags=[ REVOKE_WHEN_REQUESTED|USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED] ============================================================ CURRENT MOBILE VERSION AND DEPENDENCIES ============================================================ 1 plugins { 2 id("com.android.application") 3 id("org.jetbrains.kotlin.plugin.compose") 4 } // SHIRE-MOBILE-APPROVAL-INBOX-0001 5 6 val shireApprovalBaseUrl = providers 7 .gradleProperty("SHIRE_APPROVAL_BASE_URL") 8 .orElse("http://127.0.0.1:8788") 9 .get() 10 11 val shireApprovalToken = providers 12 .gradleProperty("SHIRE_APPROVAL_TOKEN") 13 .orElse("") 14 .get() 15 16 android { 17 namespace = "com.shire.mobile" 18 compileSdk = 36 19 20 defaultConfig { 21 applicationId = "com.shire.mobile" 22 minSdk = 29 23 targetSdk = 36 24 versionCode = 7 25 versionName = "0.6.1-private-approval-inbox-startup-fix" 26 buildConfigField( 27 "String", 28 "APPROVAL_API_BASE_URL", 29 "\"$shireApprovalBaseUrl\"" 30 ) 31 buildConfigField( 32 "String", 33 "APPROVAL_API_TOKEN", 34 "\"$shireApprovalToken\"" 35 ) 36 } 37 38 buildFeatures { 39 compose = true 40 buildConfig = true 41 } 42 43 compileOptions { 44 sourceCompatibility = JavaVersion.VERSION_17 45 targetCompatibility = JavaVersion.VERSION_17 46 } 47 48 packaging { 49 resources { 50 excludes += "/META-INF/{AL2.0,LGPL2.1}" 51 } 52 } 53 } 54 55 dependencies { 56 val composeBom = platform("androidx.compose:compose-bom:2026.06.00") 57 58 implementation(composeBom) 59 androidTestImplementation(composeBom) 60 61 implementation("androidx.activity:activity-compose:1.13.0") 62 implementation("androidx.compose.material3:material3") 63 implementation("androidx.compose.foundation:foundation") 64 implementation("androidx.compose.ui:ui") 65 implementation("androidx.compose.ui:ui-tooling-preview") 66 67 debugImplementation("androidx.compose.ui:ui-tooling") 68 } ============================================================ ANDROID MANIFEST ============================================================ 1 2 3 4 5 6 7 8 9 10 18 19 23 24 25 26 27 28 29 30 31 32 33 37 38 39 40 41 42 45 46 47 48 49 53 54 55 56 ============================================================ SENTINEL / SHIRE / VOICE SERVICES ============================================================ apparmor.service enabled enabled shire-academy-autonomous-practice.service disabled enabled shire-academy-cadquery-fast.service enabled enabled shire-academy-safe-autonomous.service disabled enabled shire-academy-ui.service enabled enabled shire-agent-hub-tailnet.service static - shire-agent-hub.service enabled enabled shire-agent@.service indirect enabled shire-approval-hub.service enabled enabled shire-brain.service enabled enabled shire-marketing-boss-tailnet.service static - shire-marketing-boss-worker.service static - shire-marketing-boss.service enabled enabled shire-marketing-cloudflare-tunnel.service enabled enabled shire-marketing-oauth-callback.service enabled enabled shire-marketing-pipeline.service enabled enabled shire-marketing-token-backup.service static - shire-marketing-token-lifecycle.service disabled enabled shire-marketing-token-reload.service disabled enabled shire-sentinel-alert.service enabled enabled shire-sentinel-suricata-update.service static - shire-sentinel-suricata.service enabled enabled shire-trendscout-tailnet.service static - shire-trendscout-weekly-intelligence.service static - shire-trendscout.service enabled enabled shire-unified-learning.service enabled enabled speech-dispatcher.service generated - speech-dispatcherd.service disabled enabled ============================================================ ACTIVE MATCHING SERVICES ============================================================ shire-academy-cadquery-fast.service loaded active running SHiRE Academy Persistent CadQuery Fast Engine shire-academy-ui.service loaded active running SHiRE Academy Local UI shire-agent-hub.service loaded active running SHiRE Specialist Agent Hub shire-agent@covercanvas.service loaded active running SHiRE Specialist Agent Runtime — covercanvas shire-agent@limbforge.service loaded active running SHiRE Specialist Agent Runtime — limbforge shire-agent@scanforge.service loaded active running SHiRE Specialist Agent Runtime — scanforge shire-approval-hub.service loaded active running SHiRE Private Approval Hub shire-brain.service loaded active running SHIRE Brain API Server shire-marketing-boss-tailnet.service loaded active running Private Tailnet proxy for SHiRE Marketing Boss shire-marketing-boss.service loaded active running SHiRE Marketing Boss Drafting Workspace shire-marketing-cloudflare-tunnel.service loaded active running SHiRE Marketing OAuth Cloudflare Tunnel shire-marketing-oauth-callback.service loaded active running SHiRE Marketing OAuth Callback-Only Gate shire-marketing-pipeline.service loaded active running SHiRE Marketing Boss Instagram Production Pipeline shire-sentinel-alert.service loaded active running SHiRE Sentinel Live Threat Alert Bridge shire-sentinel-suricata.service loaded active running SHiRE Sentinel Suricata Passive Network IDS shire-trendscout-tailnet.service loaded active running Private Tailnet proxy for SHiRE TrendScout shire-trendscout.service loaded active running SHiRE TrendScout Native Read-Only Workspace shire-unified-learning.service loaded active running SHiRE Unified Learning 0002 Local Service ============================================================ MATCHING UNIT DEFINITIONS ============================================================ UNIT: shire-approval-hub.service # /etc/systemd/system/shire-approval-hub.service [Unit] Description=SHiRE Private Approval Hub After=network-online.target tailscaled.service Wants=network-online.target Requires=tailscaled.service [Service] Type=simple User=ray Group=ray Environment=PYTHONUNBUFFERED=1 EnvironmentFile=/etc/shire-approval-hub/approval-hub.env ExecStart=/home/shire3d/ARMOR/venv/bin/python /usr/local/lib/shire-approval-hub/server.py Restart=on-failure RestartSec=5 NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=read-only ReadWritePaths=/SHiREVault/SHiREApprovals /SHiREVault/SHiREMobile/Releases ReadOnlyPaths=/SHiREVault/SHiREAcademy/Practice [Install] WantedBy=multi-user.target UNIT: shire-brain.service # /etc/systemd/system/shire-brain.service [Unit] Description=SHIRE Brain API Server After=network-online.target ollama.service Wants=network-online.target ollama.service [Service] Type=simple User=ray WorkingDirectory=/home/shire3d/ARMOR Environment=SHIRE_FAST_MODEL=shire-mini-fast:qwen3.5-4b Environment=SHIRE_DEEP_MODEL=shire-mini-deep:qwen3.5-9b Environment=SHIRE_BRAIN_PORT=8765 Environment=SHIRE_BRAIN_HOST=127.0.0.1 Environment=OLLAMA_URL=http://127.0.0.1:11434 ExecStart=/home/shire3d/ARMOR/venv/bin/python /home/shire3d/ARMOR/ai/brain_server.py Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target UNIT: shire-sentinel-alert.service # /etc/systemd/system/shire-sentinel-alert.service [Unit] Description=SHiRE Sentinel Live Threat Alert Bridge Requires=shire-sentinel-suricata.service After=shire-sentinel-suricata.service graphical.target [Service] Type=simple User=root Group=root ExecStart=/usr/bin/python3 /usr/local/lib/shire-sentinel/alert_bridge.py --config /etc/shire-sentinel/alert-bridge.json Restart=always RestartSec=3 TimeoutStopSec=20 NoNewPrivileges=true PrivateTmp=true PrivateDevices=true ProtectSystem=strict ProtectHome=read-only ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true ProtectClock=true RestrictSUIDSGID=true LockPersonality=true MemoryDenyWriteExecute=true ReadOnlyPaths=/var/log/suricata/shire-sentinel ReadWritePaths=/var/lib/shire-sentinel-alert /home/shire3d/ARMOR/data/sentinel [Install] WantedBy=multi-user.target UNIT: shire-sentinel-suricata-update.service # /etc/systemd/system/shire-sentinel-suricata-update.service [Unit] Description=Update SHiRE Sentinel Suricata Threat Rules Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/suricata-update update ExecStartPost=/bin/systemctl reload shire-sentinel-suricata.service UNIT: shire-sentinel-suricata.service # /etc/systemd/system/shire-sentinel-suricata.service [Unit] Description=SHiRE Sentinel Suricata Passive Network IDS Wants=network-online.target After=network-online.target NetworkManager.service ufw.service [Service] Type=simple ExecStart=/usr/bin/suricata --af-packet=wlp1s0 -c /etc/suricata/suricata.yaml -S /var/lib/suricata/rules/suricata.rules -l /var/log/suricata/shire-sentinel --set vars.address-groups.HOME_NET=[192.168.1.0/24] --pidfile /run/shire-sentinel-suricata.pid --init-errors-fatal ExecReload=/bin/kill -USR2 $MAINPID Restart=on-failure RestartSec=5s KillSignal=SIGINT TimeoutStopSec=60 LimitNOFILE=65536 PrivateTmp=true ProtectHome=true ProtectSystem=full ReadWritePaths=/var/log/suricata/shire-sentinel /var/lib/suricata /run NoNewPrivileges=true [Install] WantedBy=multi-user.target ============================================================ LISTENING NETWORK PORTS ============================================================ Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("adb",pid=966957,fd=13)) udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("adb",pid=966957,fd=4)) udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* udp UNCONN 0 0 0.0.0.0:54909 0.0.0.0:* udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* udp UNCONN 0 960 0.0.0.0:41641 0.0.0.0:* udp UNCONN 0 0 127.0.0.1:34773 0.0.0.0:* users:(("adb",pid=966957,fd=7)) udp UNCONN 0 0 [::]:5353 [::]:* udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=14)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=15)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=16)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=17)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=19)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=20)) udp UNCONN 0 0 *:5353 *:* users:(("adb",pid=966957,fd=21)) udp UNCONN 0 0 [::]:54886 [::]:* udp UNCONN 0 0 *:39183 *:* udp UNCONN 0 0 *:33267 *:* udp UNCONN 0 0 [fe80::5508:ae50:cc1e:8422]%wlp1s0:546 [::]:* udp UNCONN 0 0 [::]:41641 [::]:* udp UNCONN 0 0 *:50485 *:* udp UNCONN 0 0 *:34524 *:* tcp LISTEN 0 128 127.0.0.1:6012 0.0.0.0:* tcp LISTEN 0 128 127.0.0.1:6010 0.0.0.0:* tcp LISTEN 0 128 127.0.0.1:6011 0.0.0.0:* tcp LISTEN 0 128 127.0.0.1:5037 0.0.0.0:* users:(("adb",pid=966957,fd=12)) tcp LISTEN 0 511 127.0.0.1:4174 0.0.0.0:* users:(("node",pid=1742,fd=24)) tcp LISTEN 0 511 127.0.0.1:4173 0.0.0.0:* users:(("node",pid=1741,fd=24)) tcp LISTEN 0 4096 100.116.93.90:4173 0.0.0.0:* tcp LISTEN 0 511 100.116.93.90:4174 0.0.0.0:* users:(("node",pid=94292,fd=24)) tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* tcp LISTEN 0 5 127.0.0.1:8782 0.0.0.0:* users:(("python3",pid=1016963,fd=4)) tcp LISTEN 0 5 127.0.0.1:8783 0.0.0.0:* users:(("python3",pid=1016834,fd=4)) tcp LISTEN 0 5 127.0.0.1:8781 0.0.0.0:* users:(("python3",pid=27381,fd=6)) tcp LISTEN 0 5 127.0.0.1:8770 0.0.0.0:* users:(("python3",pid=1057236,fd=3)) tcp LISTEN 0 5 127.0.0.1:8792 0.0.0.0:* users:(("python",pid=1003197,fd=3)) tcp LISTEN 0 5 127.0.0.1:8790 0.0.0.0:* users:(("python3",pid=1003205,fd=3)) tcp LISTEN 0 5 127.0.0.1:8791 0.0.0.0:* users:(("python3",pid=164504,fd=3)) tcp LISTEN 0 5 127.0.0.1:8784 0.0.0.0:* users:(("python3",pid=1016942,fd=4)) tcp LISTEN 0 5 127.0.0.1:8765 0.0.0.0:* users:(("python",pid=1060559,fd=3)) tcp LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* tcp LISTEN 0 4096 127.0.0.1:20241 0.0.0.0:* tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* tcp LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:* tcp LISTEN 0 4096 100.116.93.90:33389 0.0.0.0:* tcp LISTEN 0 4096 100.116.93.90:8781 0.0.0.0:* tcp LISTEN 0 5 100.116.93.90:8788 0.0.0.0:* users:(("python",pid=1098305,fd=3)) tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* tcp LISTEN 0 128 [::1]:6012 [::]:* tcp LISTEN 0 128 [::1]:6011 [::]:* tcp LISTEN 0 128 [::1]:6010 [::]:* tcp LISTEN 0 4096 [fd7a:115c:a1e0::283b:5d5b]:63494 [::]:* tcp LISTEN 0 4096 [::]:22 [::]:* tcp LISTEN 0 4096 [::1]:631 [::]:* ============================================================ TAILSCALE STATUS ============================================================ 100.116.93.90 shire-mini 3drays85@ linux - 100.74.51.32 armoros 3drays85@ linux active; relay "syd"; offline, last seen 11d ago, tx 4524 rx 0 100.110.126.100 pixel-9-pro-fold 3drays85@ android - 100.100.231.64 shire-3d 3drays85@ windows - 100.111.247.1 shire-node-zero 3drays85@ linux offline, last seen 13d ago 100.116.93.90 ============================================================ AUDIO DEVICES ============================================================ 53 alsa_input.usb-Microsoft_Kinect_USB_Audio_A44887D16675043A-01.input-4-channels PipeWire s32le 4ch 16000Hz SUSPENDED 54 alsa_output.usb-Generic_Blue_Microphones_LT_220901120049AD0200D0_111000-00.analog-stereo.monitor PipeWire s16le 2ch 48000Hz SUSPENDED 55 alsa_input.usb-Generic_Blue_Microphones_LT_220901120049AD0200D0_111000-00.analog-stereo PipeWire s16le 2ch 48000Hz SUSPENDED 22426 alsa_output.pci-0000_00_1f.3.analog-stereo.monitor PipeWire s32le 2ch 48000Hz SUSPENDED 22427 alsa_input.pci-0000_00_1f.3.analog-stereo.2 PipeWire s32le 2ch 48000Hz SUSPENDED 54 alsa_output.usb-Generic_Blue_Microphones_LT_220901120049AD0200D0_111000-00.analog-stereo PipeWire s16le 2ch 48000Hz SUSPENDED 22426 alsa_output.pci-0000_00_1f.3.analog-stereo PipeWire s32le 2ch 48000Hz SUSPENDED **** List of CAPTURE Hardware Devices **** card 0: PCH [HDA Intel PCH], device 0: SN6140 Analog [SN6140 Analog] Subdevices: 1/1 Subdevice #0: subdevice #0 card 1: Microphones [Blue Microphones], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 card 2: Audio [Kinect USB Audio], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 **** List of PLAYBACK Hardware Devices **** card 0: PCH [HDA Intel PCH], device 0: SN6140 Analog [SN6140 Analog] Subdevices: 1/1 Subdevice #0: subdevice #0 card 0: PCH [HDA Intel PCH], device 3: HDMI 0 [AndroidTV] Subdevices: 1/1 Subdevice #0: subdevice #0 card 0: PCH [HDA Intel PCH], device 7: HDMI 1 [HDMI 1] Subdevices: 1/1 Subdevice #0: subdevice #0 card 0: PCH [HDA Intel PCH], device 8: HDMI 2 [HDMI 2] Subdevices: 1/1 Subdevice #0: subdevice #0 card 0: PCH [HDA Intel PCH], device 9: HDMI 3 [HDMI 3] Subdevices: 1/1 Subdevice #0: subdevice #0 card 1: Microphones [Blue Microphones], device 0: USB Audio [USB Audio] Subdevices: 1/1 Subdevice #0: subdevice #0 ============================================================ VOICE / STT / TTS BINARIES ============================================================ whisper-cli not found whisper-cpp not found ffmpeg /usr/bin/ffmpeg ffplay /usr/bin/ffplay arecord /usr/bin/arecord aplay /usr/bin/aplay pw-record /usr/bin/pw-record pw-play /usr/bin/pw-play pactl /usr/bin/pactl ollama /usr/local/bin/ollama ============================================================ KNOWN VOICE AND MODEL FILES ============================================================ /home/ray/.steam/debian-installation/friends/VoiceChatInviteNotification.res /home/ray/.steam/debian-installation/friends/voice_hang_up.wav /home/ray/.steam/debian-installation/friends/voicebar.res /home/ray/.steam/debian-installation/public/steam_voice.ico /home/ray/.steam/debian-installation/resource/voice_busy.wav /home/ray/.steam/debian-installation/resource/voice_dialing.wav /home/ray/.steam/debian-installation/resource/voice_ringing.wav /home/ray/.steam/debian-installation/steam/cached/SettingsSubVoice.res /home/ray/.steam/debian-installation/ubuntu12_32/mssvoice.asi /home/ray/Downloads/SHIRE-MOBILE-SENTINEL-CHAT-VOICE-PIXEL-CAPTURE.sh /home/ray/Downloads/SHIRE-MOBILE-SENTINEL-CHAT-VOICE-PIXEL-CAPTURE.sh.sha256 /home/ray/Downloads/SHIRE-MOBILE-SENTINEL-CHAT-VOICE-SOURCE-CAPTURE.sh /home/ray/Downloads/SHIRE-MOBILE-SENTINEL-CHAT-VOICE-SOURCE-CAPTURE.sh.sha256 /home/shire3d/ARMOR/backups/ENG-UTIL-0006A_keyboard_voice_guard_UNFINISHED_20260708_141039.tar.gz /home/shire3d/ARMOR/backups/parked_patches/ENG-UTIL-0006A_keyboard_voice_guard_UNFINISHED_20260708_141039.patch /home/shire3d/ARMOR/config/voice_privacy.json /home/shire3d/ARMOR/core/__pycache__/voice_privacy.cpython-312.pyc /home/shire3d/ARMOR/core/voice_privacy.py /home/shire3d/ARMOR/docs/voice/SHIRE_VOICE_0002A_GLOBAL_PROMPT_BAR.md /home/shire3d/ARMOR/models/vision/face/face_detection_yunet_2023mar.onnx /home/shire3d/ARMOR/models/vision/face/face_recognition_sface_2021dec.onnx ============================================================ SENTINEL SOURCE OUTLINE ============================================================ 1:from PyQt5.QtWidgets import * 2:from PyQt5.QtCore import * 3:from PyQt5.QtGui import * 5:import shutil 6:import subprocess 7:import time 8:from pathlib import Path 10:from framework.warden_scroll import WardenTouchScrollArea 11:from framework.warden_theme import * 12:from framework.warden_widgets import * 13:from core.service_manager import service_manager 14:from core.event_bus import event_bus 17:class SentinelTile(QFrame): 96: def mousePressEvent(self, event): 97: self._press_pos = event.pos() 99: super().mousePressEvent(event) 101: def mouseReleaseEvent(self, event): 104: dx = abs(event.pos().x() - self._press_pos.x()) 105: dy = abs(event.pos().y() - self._press_pos.y()) 109: super().mouseReleaseEvent(event) 112:class SentinelPage(WardenBackground): 122: event_bus.publish("sentinel", "boot", "Sentinel Security Grid online", "online") 263: "network": SentinelTile("network", "◎", "NETWORK", "CHECK"), 264: "ports": SentinelTile("ports", "▦", "OPEN PORTS", "CHECK"), 267: "firewall": SentinelTile("firewall", "▣", "FIREWALL", "CHECK"), 271: order = ["network", "ports", "capture", "malware", "firewall", "harden"] 293: ("NETWORK", self.network_status), 294: ("OPEN PORTS", self.port_status), 341: "PROTECTION: UFW package installed but firewall INACTIVE | Fail2Ban ACTIVE\n" 343: "BOUNDARY: observe, report, protect only." 408: def paintEvent(self, event): 424: super().paintEvent(event) 582: required = ["tshark", "tcpdump", "clamscan", "ufw", "lynis", "yara", "fail2ban-client"] 592: self.tiles["malware"].set_value("CLAMAV READY" if self.command_ready("clamscan") else "NO AV YET") 595: self.tiles["firewall"].set_value(f"UFW {ufw}" if ufw != "NOT INSTALLED" else "NEEDS UFW") 605: self.tiles["network"].set_value("ONLINE" if "UP" in out else "CHECK") 607: code, ports = self.run_cmd(["ss", "-tuln"], timeout=3) 608: count = max(0, len([line for line in ports.splitlines() if "LISTEN" in line])) 609: self.tiles["ports"].set_value(f"{count} OPEN") 614: if key == "network": 615: self.network_status() 616: elif key == "ports": 617: self.port_status() 622: elif key == "firewall": 623: self.firewall_status() 640: ("ClamAV", "clamscan"), 650: self.log(f" UFW firewall state: {self.ufw_state()}") 657: self.log("UFW firewall is still INACTIVE until Ray approves firewall rules.") 659: self.log("Sentinel boundary: observe, report, protect. No offensive actions.") 661: def network_status(self): 666: def port_status(self): 670: self.log("\n".join(lines[:18]) if lines else "No listening port output returned.") 683: if shutil.which("clamscan"): 688: def firewall_status(self): ============================================================ RELEVANT ARMOR SOURCE FILES ============================================================ /home/shire3d/ARMOR/agents/apps/covercanvas/package-lock.json /home/shire3d/ARMOR/agents/apps/limbforge/package-lock.json /home/shire3d/ARMOR/agents/apps/scanforge/dist/shire-module/shire-agent-manifest.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/.package-lock.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/@tanstack/react-query/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/asynckit/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/axios/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/baseline-browser-mapping/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/character-entities-html4/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/character-entities-legacy/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/character-entities/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/character-reference-invalid/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/combined-stream/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/delayed-stream/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/engine.io-client/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/fast-glob/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/follow-redirects/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/form-data/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/is-core-module/core.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/merge2/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/micromark/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/mime-db/db.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/mime-db/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/react-markdown/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/readdirp/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/rollup/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/socket.io-client/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/svg-pathdata/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/three/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/vite/package.json /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/ws/package.json /home/shire3d/ARMOR/agents/apps/scanforge/package-lock.json /home/shire3d/ARMOR/agents/apps/scanforge/public/shire-agent-manifest.json /home/shire3d/ARMOR/agents/definitions/covercanvas/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/flexiforge/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/limbforge/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/marketing-boss/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/scanforge/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/store-doctor/agent.manifest.json /home/shire3d/ARMOR/agents/definitions/trendscout/agent.manifest.json /home/shire3d/ARMOR/agents/runtime/agent_hub.py /home/shire3d/ARMOR/agents/runtime/specialist_agent_runtime.py /home/shire3d/ARMOR/ai/brain_server.py /home/shire3d/ARMOR/ai/systemd/shire-academy-practice.service /home/shire3d/ARMOR/ai/systemd/shire-brain.service /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/@tanstack/react-query/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/baseline-browser-mapping/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/fast-glob/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/is-core-module/core.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/merge2/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/readdirp/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/rollup/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/cs/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/de/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/fr/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/it/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/ja/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/ko/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/pl/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/ru/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/tr/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/vite/package.json /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/scripts/shire_academy_server.py /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json /home/shire3d/ARMOR/apps/shire-academy/runtime/boolean_canary_status.json /home/shire3d/ARMOR/apps/shire-academy/server/shire_academy_server.py /home/shire3d/ARMOR/apps/shire-academy/server/stage3c7_safe_plans.py /home/shire3d/ARMOR/apps/shire-academy/server/unified_learning_proxy.py /home/shire3d/ARMOR/apps/shire-unified-learning/server/unified_learning_server.py /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json /home/shire3d/ARMOR/backups/ENG-LAPTOP-0004J_20260706_115217/ai.bak/brain_server.py /home/shire3d/ARMOR/backups/ENG-LAPTOP-0005A_20260706_130421/ai.bak/brain_server.py /home/shire3d/ARMOR/backups/ENG-LAPTOP-0005A_20260706_130421/ai.bak/systemd/shire-brain.service /home/shire3d/ARMOR/backups/ENG-SEC-0002A_20260708_143637/shire.py /home/shire3d/ARMOR/config/academy.json /home/shire3d/ARMOR/config/agents/registry.json /home/shire3d/ARMOR/config/shire_appliances.json /home/shire3d/ARMOR/config/vision_modes.json /home/shire3d/ARMOR/config/voice_privacy.json /home/shire3d/ARMOR/core/event_bus.py /home/shire3d/ARMOR/core/service_manager.py /home/shire3d/ARMOR/core/voice_privacy.py /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json /home/shire3d/ARMOR/data/academy/design_mastery/config/route_keywords.json /home/shire3d/ARMOR/data/academy/design_mastery/curriculum/catalog.json /home/shire3d/ARMOR/data/academy/design_mastery/research/adapters/github_repository_search.py /home/shire3d/ARMOR/data/academy/design_mastery/research/manual_source_template.json /home/shire3d/ARMOR/data/academy/design_mastery/research/source_registry.json /home/shire3d/ARMOR/data/academy/design_mastery/runtime/practice_state.json /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/certification.json /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/manifest.json /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/tests.json /home/shire3d/ARMOR/data/academy/design_mastery/tools/licence_gate.py /home/shire3d/ARMOR/data/forge_learning_queue.json /home/shire3d/ARMOR/data/parametric_forge_projects/CAL-0001/manifest.json /home/shire3d/ARMOR/data/sentinel/alert_bridge_status.json /home/shire3d/ARMOR/data/shire3d_commercial_projects/COMM-0001/asset-source.json /home/shire3d/ARMOR/heart/heart.py /home/shire3d/ARMOR/main.py /home/shire3d/ARMOR/modules/agents.py /home/shire3d/ARMOR/modules/dashboard.py /home/shire3d/ARMOR/modules/global_prompt_bar.py /home/shire3d/ARMOR/modules/laptop_dashboard.py /home/shire3d/ARMOR/modules/sentinel.py /home/shire3d/ARMOR/modules/shire.py /home/shire3d/ARMOR/modules/unified_learning.py /home/shire3d/ARMOR/scripts/blender_mastery_questions.py /home/shire3d/ARMOR/scripts/shire_live_transcribe.py /home/shire3d/ARMOR/sentinel/status.py /home/shire3d/ARMOR/services/ai_service.py /home/shire3d/ARMOR/services/blender_brain_contract.py /home/shire3d/ARMOR/services/blender_mastery_state.py /home/shire3d/ARMOR/services/design_academy_practice_service.py /home/shire3d/ARMOR/services/forge_learning_service.py /home/shire3d/ARMOR/services/tasks.py /home/shire3d/ARMOR/services/vision_mode_service.py /home/shire3d/ARMOR/services/wake_word_service.py /home/shire3d/ARMOR/tests/test_blender_brain_contract.py /home/shire3d/ARMOR/tests/test_blender_mastery_questions_cli.py /home/shire3d/ARMOR/tests/test_blender_mastery_state.py /home/shire3d/ARMOR/tools/design_academy_autopilot_qualify.py /home/shire3d/ARMOR/tools/design_academy_counted_round3_recovery.py /home/shire3d/ARMOR/tools/design_academy_generic_qualify.py /home/shire3d/ARMOR/tools/design_academy_mass_learning_infrastructure_selftest.py /home/shire3d/ARMOR/tools/design_academy_micro_exam_selftest.py /home/shire3d/ARMOR/tools/design_academy_observed_round1_selftest.py /home/shire3d/ARMOR/tools/design_academy_round2_anti_echo_selftest.py /home/shire3d/ARMOR/tools/design_academy_round2_completeness_selftest.py /home/shire3d/ARMOR/tools/design_academy_round3_timeout_selftest.py /home/shire3d/ARMOR/tools/design_academy_round_contract_selftest.py /home/shire3d/ARMOR/tools/design_academy_structured_output_selftest.py /home/shire3d/ARMOR/tools/design_academy_timeout_recovery_selftest.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio-4.14.1.dist-info/scm_file_list.json /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_core/_streams.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/abc/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/abc/_streams.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/buffered.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/file.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/memory.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/stapled.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/text.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/streams/tls.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/anyio/to_process.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/utils.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/x509/name.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_connection.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_events.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_receivebuffer.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_util.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_api.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/connection.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/connection_pool.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/http11.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/http2.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_backends/trio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_models.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/connection.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/http2.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_api.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_client.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_content.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_decoders.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_exceptions.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_main.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_models.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_multipart.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_transports/asgi.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_transports/base.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_transports/default.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_transports/wsgi.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/httpx/_utils.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/idna/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/idna/cli.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/idna/codec.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/idna/core.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/_globals.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/_utils/_inspect.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/array_api/_array_object.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/array_api/_typing.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/compat/py3k.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/core/_add_newdocs.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/core/_internal.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/core/fromnumeric.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/_isocbind.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/auxfuncs.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/capi_maps.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/cb_rules.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/cfuncs.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/common_rules.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/crackfortran.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/f2py2e.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/f90mod_rules.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/func2subr.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/rules.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/setup.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/symbolic.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/f2py/use_rules.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/lib/_iotools.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/lib/format.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/numpy/lib/npyio.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_types.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/packaging/_tokenizer.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/packaging/licenses/_spdx.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/packaging/specifiers.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/packaging/tags.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pip/_internal/build_env.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pip/_vendor/__init__.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pycparser/c_lexer.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pycparser/c_parser.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_decorators.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_utils.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/class_validators.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/config.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/decorator.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/json.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/parse.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/deprecated/tools.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/experimental/missing_sentinel.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/json_schema.py /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/type_adapter.py ============================================================ RELEVANT SOURCE EXCERPTS ============================================================ /home/shire3d/ARMOR/services/vision_mode_service.py:117: conversation_active=False, /home/shire3d/ARMOR/services/vision_mode_service.py:134: permitted = bool(conversation_active) /home/shire3d/ARMOR/services/wake_word_service.py:5:This module cannot open a microphone, execute PocketSphinx, retain audio, /home/shire3d/ARMOR/services/wake_word_service.py:32: microphone permission, recognition permission, explicit wake-word /home/shire3d/ARMOR/services/wake_word_service.py:84: "microphone_access_enabled", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:29: "sentinel_mode_blue_optional": true, /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:172: "file": "assets/shire/expressions/shire_sentinel_standby.png", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:173: "id": "sentinel_standby", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:186: "file": "assets/shire/expressions/shire_sentinel_active.png", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:187: "id": "sentinel_active", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:200: "file": "assets/shire/expressions/shire_sentinel_lockdown.png", /home/shire3d/ARMOR/apps/shire-academy/dist/assets/shire/expressions/expression_manifest.json:201: "id": "sentinel_lockdown", /home/shire3d/ARMOR/apps/shire-unified-learning/server/unified_learning_server.py:567: ollama_url + "/api/chat", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:424: "Checking_if_0_is_the_longest_matching_prefix_for_1_2_6104": "Comprobando si '{0}' es el prefijo coincidente más largo para '{1}' - '{2}'.", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:532: "Could_not_find_matching_access_expressions_95141": "No se encontraron expresiones de acceso coincidentes.", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:1018: "Longest_matching_prefix_for_0_is_1_6108": "El prefijo coincidente más largo para \"{0}\" es \"{1}\".", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:1025: "Matched_0_condition_1_6403": "Coincidente con '{0}' condición '{1}'.", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:1057: "Module_name_0_matched_pattern_1_6092": "Nombre del módulo: '{0}', patrón coincidente: '{1}'.", /home/shire3d/ARMOR/apps/shire-academy-full-ui-staging/node_modules/typescript/lib/es/diagnosticMessages.generated.json:1437: "Saw_non_matching_condition_0_6405": "Se vio una condición no coincidente '{0}'.", /home/shire3d/ARMOR/heart/heart.py:32: "I do not exist as a generic chatbot. I am the voice and mind of the Forge.\n\n" /home/shire3d/ARMOR/data/sentinel/alert_bridge_status.json:4: "watching": "/var/log/suricata/shire-sentinel/eve.json", /home/shire3d/ARMOR/data/academy/design_mastery/runtime/practice_state.json:1887: "electronics_integration.microphone-mounts": { /home/shire3d/ARMOR/data/academy/design_mastery/runtime/practice_state.json:1899: "skill_id": "electronics_integration.microphone-mounts", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2646: "path": "skills/electronics_integration/microphone-mounts/SKILL.md", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2650: "path": "skills/electronics_integration/microphone-mounts/certification.json", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2654: "path": "skills/electronics_integration/microphone-mounts/design_rules.yaml", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2658: "path": "skills/electronics_integration/microphone-mounts/manifest.json", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2662: "path": "skills/electronics_integration/microphone-mounts/tests.json", /home/shire3d/ARMOR/data/academy/design_mastery/FILE_INVENTORY.json:2666: "path": "skills/electronics_integration/microphone-mounts/workflow.yaml", /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/certification.json:2: "skill_id": "electronics_integration.microphone-mounts", /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/tests.json:2: "skill_id": "electronics_integration.microphone-mounts", /home/shire3d/ARMOR/data/academy/design_mastery/skills/electronics_integration/microphone-mounts/manifest.json:2: "skill_id": "electronics_integration.microphone-mounts", /home/shire3d/ARMOR/data/academy/design_mastery/config/route_keywords.json:29: "microphone", /home/shire3d/ARMOR/data/academy/design_mastery/curriculum/catalog.json:1723: "skill_id": "electronics_integration.microphone-mounts", /home/shire3d/ARMOR/sentinel/status.py:55: "Sentinel": Path("/home/shire3d/ARMOR/sentinel/status.py").exists(), /home/shire3d/ARMOR/ai/brain_server.py:714: OLLAMA_URL + "/api/chat", /home/shire3d/ARMOR/ai/brain_server.py:926: OLLAMA_URL + "/api/chat", /home/shire3d/ARMOR/scripts/shire_live_transcribe.py:72: "PipeWire returned no default microphone source." /home/shire3d/ARMOR/scripts/shire_live_transcribe.py:157: "Unable to create microphone process pipes." /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:94: 'sentinel', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:176:# Placeholder for sentinel methods, because sentinels can not have their own sentinels /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:177:_sentinel_placeholder = object() /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:179:if hasattr(builtins, "sentinel"): # 3.15+ /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:180: sentinel = builtins.sentinel /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:182: class sentinel: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:183: """Create a unique sentinel object. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:191: __name: str = _sentinel_placeholder, /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:192: __repr: typing.Optional[str] = _sentinel_placeholder, /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:196: name: str = _sentinel_placeholder, /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:198: if name is not _sentinel_placeholder: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:206: if __name is _sentinel_placeholder: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:208: if __repr is not _sentinel_placeholder: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:225: "Subclassing sentinel is deprecated " /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:235: f"Setting attribute {attr!r} on sentinel objects is deprecated " /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:268: """Reduce this sentinel to a singleton.""" /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:271:Sentinel = sentinel /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_extensions.py:273:_marker = sentinel("sentinel") /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/urllib3/util/timeout.py:106: #: A sentinel object representing the default timeout value /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/urllib3/util/timeout.py:198: # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/urllib3/connection.py:423: # urllib3's API is different from `http.client.HTTPConnection` and the subclassing is only incidental. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_inspection/introspection.py:357:"""A sentinel value used when no [type expression][] is present.""" /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_inspection/introspection.py:360:"""The type of the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel value.""" /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_inspection/introspection.py:369: If no type expression is available, the [`UNKNOWN`][typing_inspection.introspection.UNKNOWN] sentinel /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_inspection/typing_objects.py:364:Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/typing_inspection/typing_objects.py:374:Return whether the argument is the `NoExtraItems` sentinel object. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:309: def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:325: def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:340: def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:355: Create a chat response using the requested model. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:379: client.chat(model='llama3.2', tools=[add_two_numbers], messages=[...]) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:390: '/api/chat', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:941: async def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:957: async def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:972: async def chat( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:987: Create a chat response using the requested model. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:1011: await client.chat(model='llama3.2', tools=[add_two_numbers], messages=[...]) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_client.py:1023: '/api/chat', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_types.py:398: 'Messages to chat with.' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_types.py:401: 'Tools to use for the chat.' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/_types.py:415: Response returned by chat requests. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/ollama/__init__.py:47:chat = _client.chat /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_events.py:1:# High level events that make up HTTP/1.1 conversations. Loosely inspired by /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/h11/_util.py:99:# - Have a *bonus property*: type(sentinel) is sentinel /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/utils.py:115: sentinel = object() /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/utils.py:118: cache = getattr(instance, cached_name, sentinel) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/utils.py:119: if cache is not sentinel: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/cryptography/x509/name.py:169: # alternate types. This means when we see the sentinel value we need /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:1373: type: Required[Literal['missing-sentinel']] /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:1378:def missing_sentinel_schema( /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:1382: """Returns a schema for the `MISSING` sentinel.""" /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:1385: type='missing-sentinel', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:4196: 'missing-sentinel', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic_core/core_schema.py:4298: 'missing_sentinel_error', /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_utils.py:382: """Wrapper redirecting `__getitem__` to `get` with a sentinel value as default /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_decorators.py:356:_sentinel = object() /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_decorators.py:380: attribute = base.__dict__.get(name, _sentinel) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_decorators.py:381: if attribute is not _sentinel: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:1093: return core_schema.missing_sentinel_schema() /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:2310: choice['type'] == 'missing-sentinel' for choice in core_schema.iter_union_choices(schema) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:2316: if (choice[0] if isinstance(choice, tuple) else choice)['type'] != 'missing-sentinel' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:2329: if (choice[0] if isinstance(choice, tuple) else choice)['type'] == 'missing-sentinel' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:2343: if isinstance(choice, tuple) and choice[0]['type'] != 'missing-sentinel' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/_internal/_generate_schema.py:2345: if not isinstance(choice, tuple) and choice['type'] != 'missing-sentinel' /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/v1/env_settings.py:13:env_file_sentinel = str(object()) /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/v1/env_settings.py:33: _env_file: Optional[DotenvType] = env_file_sentinel, /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/v1/env_settings.py:61: env_file=(_env_file if _env_file != env_file_sentinel else self.__config__.env_file), /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/json_schema.py:112:"""A sentinel value used to indicate that no default value should be used when generating a JSON Schema /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/json_schema.py:870: def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue: /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/json_schema.py:871: """Generates a JSON schema that matches the `MISSING` sentinel value. /home/shire3d/ARMOR/venv/lib/python3.12/site-packages/pydantic/experimental/missing_sentinel.py:1:"""Experimental module exposing a function a `MISSING` sentinel.""" /home/shire3d/ARMOR/config/academy.json:16: "chat_composer_visible": true, /home/shire3d/ARMOR/config/vision_modes.json:19: "contextual_phrase_requires_active_conversation": true, /home/shire3d/ARMOR/config/voice_privacy.json:21: "microphone_access_enabled": true, /home/shire3d/ARMOR/config/voice_privacy.json:24: "text_to_speech_enabled": false, /home/shire3d/ARMOR/tools/design_academy_generic_qualify.py:35:MODEL_URL = "http://127.0.0.1:11434/api/chat" /home/shire3d/ARMOR/tools/design_academy_autopilot_qualify.py:14:MODEL_URL = "http://127.0.0.1:11434/api/chat" /home/shire3d/ARMOR/modules/laptop_dashboard.py:257: "sentinel_standby": "shire_sentinel_standby.png", /home/shire3d/ARMOR/modules/laptop_dashboard.py:258: "sentinel_active": "shire_sentinel_active.png", /home/shire3d/ARMOR/modules/laptop_dashboard.py:259: "sentinel_lockdown": "shire_sentinel_lockdown.png", /home/shire3d/ARMOR/modules/laptop_dashboard.py:991: self.aios_conversation = QTextEdit() /home/shire3d/ARMOR/modules/laptop_dashboard.py:992: self.aios_conversation.setReadOnly(True) /home/shire3d/ARMOR/modules/laptop_dashboard.py:993: self.aios_conversation.setPlainText( /home/shire3d/ARMOR/modules/laptop_dashboard.py:998: self.aios_conversation.setStyleSheet( /home/shire3d/ARMOR/modules/laptop_dashboard.py:1012: self.aios_conversation, /home/shire3d/ARMOR/modules/laptop_dashboard.py:1023: self.aios_conversation.setPlainText( /home/shire3d/ARMOR/modules/laptop_dashboard.py:1038: self.aios_conversation.setPlainText( /home/shire3d/ARMOR/modules/laptop_dashboard.py:1043: bar = self.aios_conversation.verticalScrollBar() /home/shire3d/ARMOR/modules/agents.py:48: "id": "relay", /home/shire3d/ARMOR/modules/agents.py:58: "route": "/agents/relay", /home/shire3d/ARMOR/modules/agents.py:60: "appPath": "agents/apps/relay", /home/shire3d/ARMOR/modules/agents.py:95: "Live SHiRE companion shell with chat, device connection, " /home/shire3d/ARMOR/modules/global_prompt_bar.py:178: self.microphone_button = QPushButton("🎙 MIC") /home/shire3d/ARMOR/modules/global_prompt_bar.py:179: self.microphone_button.setMinimumWidth(104) /home/shire3d/ARMOR/modules/global_prompt_bar.py:180: self.microphone_button.clicked.connect( /home/shire3d/ARMOR/modules/global_prompt_bar.py:203: layout.addWidget(self.microphone_button) /home/shire3d/ARMOR/modules/global_prompt_bar.py:333: self.microphone_button.setText( /home/shire3d/ARMOR/modules/global_prompt_bar.py:370: self.microphone_button.setText( /home/shire3d/ARMOR/modules/global_prompt_bar.py:611: self.microphone_button.setText( /home/shire3d/ARMOR/modules/shire.py:84:from sentinel.status import check_system, forge_score /home/shire3d/ARMOR/modules/sentinel.py:121: service_manager.register("sentinel", "Sentinel Security", "online") /home/shire3d/ARMOR/modules/sentinel.py:122: event_bus.publish("sentinel", "boot", "Sentinel Security Grid online", "online") /home/shire3d/ARMOR/modules/sentinel.py:186: self.helmet_path = Path(__file__).resolve().parent.parent / "assets" / "shire" / "shire_face_sentinel.png" /home/shire3d/ARMOR/modules/dashboard.py:195: ("sentinel", "WATCH", "ONLINE", "online", "♜", "SENTINEL"), /home/shire3d/ARMOR/modules/dashboard.py:640: "sentinel": (10, "Sentinel Security"), /home/shire3d/ARMOR/modules/dashboard.py:711: if key == "sentinel": /home/shire3d/ARMOR/modules/dashboard.py:713: f"Sentinel service: {service_manager.get_state('sentinel').upper()}", /home/shire3d/ARMOR/modules/dashboard.py:802: "sentinel", /home/shire3d/ARMOR/modules/dashboard.py:889: self.cards["sentinel"].set_value(service_manager.get_state("sentinel").upper(), service_manager.get_state("sentinel")) /home/shire3d/ARMOR/backups/ENG-LAPTOP-0005A_20260706_130421/ai.bak/brain_server.py:68: f"{OLLAMA_URL}/api/chat", /home/shire3d/ARMOR/backups/ENG-LAPTOP-0004J_20260706_115217/ai.bak/brain_server.py:68: f"{OLLAMA_URL}/api/chat", /home/shire3d/ARMOR/backups/ENG-SEC-0002A_20260708_143637/shire.py:18:from sentinel.status import check_system, forge_score /home/shire3d/ARMOR/agents/runtime/agent_hub.py:2:"""SHiRE Agent Hub candidate: local-only registry, health, scoped chat and task queues.""" /home/shire3d/ARMOR/agents/runtime/agent_hub.py:305:def chat(agent: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]: /home/shire3d/ARMOR/agents/runtime/agent_hub.py:318: f"{OLLAMA_URL}/api/chat", /home/shire3d/ARMOR/agents/runtime/agent_hub.py:338: path = DATA_ROOT / str(agent["id"]) / "memory" / "conversation.jsonl" /home/shire3d/ARMOR/agents/runtime/agent_hub.py:414: match = re.fullmatch(r"/api/v1/agents/([a-z0-9-]+)/(chat|tasks)", path) /home/shire3d/ARMOR/agents/runtime/agent_hub.py:422: result = chat(item, payload) if match.group(2) == "chat" else queue_task(str(item["id"]), payload) /home/shire3d/ARMOR/agents/apps/scanforge/public/shire-agent-manifest.json:17: "transport": ["same-origin-https", "authenticated-websocket", "tailscale-private-network"], /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/socket.io-client/package.json:8: "websocket", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/engine.io-client/package.json:77: "./build/esm/transports/websocket.node.js": "./build/esm/transports/websocket.js", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/engine.io-client/package.json:80: "./build/cjs/transports/websocket.node.js": "./build/cjs/transports/websocket.js", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/ws/package.json:4: "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/ws/package.json:13: "homepage": "https://github.com/websockets/ws", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/ws/package.json:14: "bugs": "https://github.com/websockets/ws/issues", /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/ws/package.json:17: "url": "git+https://github.com/websockets/ws.git" /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/mime-db/db.json:5706: "application/x-chat": { /home/shire3d/ARMOR/agents/apps/scanforge/node_modules/mime-db/db.json:5708: "extensions": ["chat"] /home/shire3d/ARMOR/agents/apps/scanforge/dist/shire-module/shire-agent-manifest.json:17: "transport": ["same-origin-https", "authenticated-websocket", "tailscale-private-network"], /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:29: "sentinel_mode_blue_optional": true, /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:172: "file": "assets/shire/expressions/shire_sentinel_standby.png", /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:173: "id": "sentinel_standby", /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:186: "file": "assets/shire/expressions/shire_sentinel_active.png", /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:187: "id": "sentinel_active", /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:200: "file": "assets/shire/expressions/shire_sentinel_lockdown.png", /home/shire3d/ARMOR/assets/shire/expressions/expression_manifest.json:201: "id": "sentinel_lockdown", /home/shire3d/ARMOR/core/event_bus.py:9: "sentinel": "online", /home/shire3d/ARMOR/core/voice_privacy.py:3:SHIRE-VOICE-0002A permits manual microphone capture for local speech /home/shire3d/ARMOR/core/voice_privacy.py:45: microphone_access_enabled: bool /home/shire3d/ARMOR/core/voice_privacy.py:54: """Validated policy plus in-memory microphone runtime state.""" /home/shire3d/ARMOR/core/voice_privacy.py:102: microphone_enabled = bool( /home/shire3d/ARMOR/core/voice_privacy.py:104: "microphone_access_enabled", /home/shire3d/ARMOR/core/voice_privacy.py:150: and not microphone_enabled /home/shire3d/ARMOR/core/voice_privacy.py:154: "microphone access." /home/shire3d/ARMOR/core/voice_privacy.py:179: microphone_enabled /home/shire3d/ARMOR/core/voice_privacy.py:222: snapshot.microphone_access_enabled /home/shire3d/ARMOR/core/voice_privacy.py:329: microphone_access_enabled=bool( /home/shire3d/ARMOR/core/voice_privacy.py:331: "microphone_access_enabled", /home/shire3d/ARMOR/core/service_manager.py:55: self.register("sentinel", "Sentinel", "online") /home/shire3d/ARMOR/main.py:65:from modules.sentinel import SentinelPage ============================================================ MOBILE FEATURE MATCHES ============================================================ /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:99: Sentinel, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:106: Sentinel, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:122:private data class ApprovalItem( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:139: ApprovalNotifications.ensureChannel(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:140: ApprovalPollJobService.schedule(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:141: ApprovalNotifications.checkNow(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:181: var showApprovals by remember { mutableStateOf(false) } /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:182: var message by remember { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:193: message = "Calm command mode ready, Chief." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:199: message = "Academy mode ready. Let us review what I have learned." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:202: ShireMode.Sentinel -> { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:204: expression = ShireExpression.Sentinel /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:205: message = "Sentinel mode active. Security systems have priority." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:211: message = "Forge mode ready. Let us build something remarkable." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:217: mutableStateOf(emptyList()) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:234: message = message, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:243: onMessageChange = { message = it }, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:248: onApprovals = { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:253: ApprovalInboxActivity::class.java /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:258: approvals = approvals + recommendedApproval(approvals) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:260: message = "Learning recommendation added for approval." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:269: message = message, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:275: onMessageChange = { message = it }, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:280: onApprovals = { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:285: ApprovalInboxActivity::class.java /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:290: approvals = approvals + recommendedApproval(approvals) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:292: message = "Learning recommendation added for approval." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:306: approvals = approvals + ApprovalItem( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:319: message = "Safe Plan added to the Approval Centre." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:324: if (showApprovals) { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:325: ApprovalCentreDialog( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:328: showApprovals = false /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:343: message = when (decision) { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:348: else -> "Approval updated." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:355:private fun recommendedApproval( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:356: approvals: List /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:357:): ApprovalItem { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:358: return ApprovalItem( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:376: message: String, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:377: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:384: onApprovals: () -> Unit, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:427: text = message, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:436: pendingApprovals = approvals.count { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:444: pendingApprovals = approvals.count { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:448: onApprovals = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:492: onApprovals = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:507: message: String, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:508: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:516: onApprovals: () -> Unit, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:552: text = message, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:566: pendingApprovals = approvals.count { it.status == "Pending" }, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:568: onApprovals = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:605: onApprovals = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:614: onApprovals = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:650: ShireMode.Sentinel -> "Sentinel" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:672: subtitle = "Daily assistance, conversation and mission control." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:678: "Private routing" to "Tailscale only" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:694: ShireMode.Sentinel -> { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:695: title = "Sentinel" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:696: subtitle = "Security posture, network alerts and protected actions." /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:701: "Network control" to "Approval required", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:713: "Print actions" to "Approval locked", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:859: ShireExpression.Sentinel -> R.drawable.shire_face_sentinel /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:904:private fun CompactStatus(pendingApprovals: Int) { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:951: pendingApprovals: Int, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:953: onApprovals: () -> Unit, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:998: onClick = onApprovals, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1144: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1146: onApprovals: () -> Unit, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1162: text = "SHiRE Mini • private Tailscale command workspace", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1182: ApprovalSummaryCard( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1184: onOpen = onApprovals /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1205: MetricCard("$approvals", "Approvals") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1280:private fun ApprovalSummaryCard( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1281: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1319: onApprovals: () -> Unit, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1390: OutlinedButton(onClick = onApprovals) { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1391: Text("Approvals") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1693: Text("Add for Approval") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1711: GuardrailLine("Certification", "Approval required") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1927:private fun ApprovalCentreDialog( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1928: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1947: text = "Approval Centre", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1978: ApprovalDetail("STAGE", item.stage, Amber) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1979: ApprovalDetail("PURPOSE", item.purpose, TextPrimary) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1980: ApprovalDetail("AFFECTS", item.affects, Blue) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1981: ApprovalDetail("RISK", item.risk, Red) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1982: ApprovalDetail("ESTIMATE", item.estimate, Green) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1983: ApprovalDetail("ROLLBACK", item.rollback, TextMuted) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1984: ApprovalDetail("STATUS", item.status, Amber) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:1988: text = "Approval applies only to this exact stage.", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt:2049:private fun ApprovalDetail( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:79:private val ApprovalBackground = Color(0xFF090B10) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:80:private val ApprovalSurface = Color(0xFF141923) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:81:private val ApprovalRaised = Color(0xFF1B2330) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:82:private val ApprovalPurple = Color(0xFFB174FF) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:83:private val ApprovalBlue = Color(0xFF53B7FF) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:84:private val ApprovalGreen = Color(0xFF20E887) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:85:private val ApprovalAmber = Color(0xFFF2B84B) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:86:private val ApprovalRed = Color(0xFFFF5A67) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:87:private val ApprovalText = Color(0xFFE8FFF2) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:88:private val ApprovalMuted = Color(0xFF91A0AA) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:90:internal data class LiveApprovalFile( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:99:internal data class LiveApproval( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:116: val files: List /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:119:class ApprovalInboxActivity : ComponentActivity() { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:126: ApprovalNotifications.ensureChannel(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:127: ApprovalPollJobService.schedule(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:128: ApprovalNotifications.checkNow(this) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:138: val requestedApprovalId = intent /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:146: primary = ApprovalPurple, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:147: secondary = ApprovalBlue, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:148: background = ApprovalBackground, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:149: surface = ApprovalSurface, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:151: onBackground = ApprovalText, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:152: onSurface = ApprovalText /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:157: color = ApprovalBackground /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:159: ApprovalInboxScreen( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:160: requestedApprovalId = requestedApprovalId, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:169:internal object ApprovalApi { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:176: "Approval Hub must use the private Tailscale HTTP endpoint" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:178: val host = uri.host ?: error("Approval Hub host is missing") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:180: "Approval Hub refused a non-Tailscale endpoint" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:183: "Approval Hub token is missing" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:233: error(payload.optString("error", "Approval Hub HTTP $code")) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:241: fun fetchApprovals(): List { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:246: add(parseApproval(rows.getJSONObject(index))) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:255: ): LiveApproval { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:265: return parseApproval(response.getJSONObject("approval")) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:280: private fun parseApproval(row: JSONObject): LiveApproval { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:281: return LiveApproval( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:283: title = row.optString("title", "Approval required"), /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:313: private fun JSONArray?.toFileList(): List { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:319: LiveApprovalFile( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:339:internal object ApprovalNotifications { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:341: private const val CHANNEL_NAME = "SHiRE Approvals" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:358: notifyPending(context, ApprovalApi.fetchApprovals()) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:365: approvals: List /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:392: ApprovalInboxActivity::class.java /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:407: .setContentTitle("Approval required: ${approval.title}") /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:428:class ApprovalPollJobService : JobService() { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:432: ApprovalNotifications.notifyPending( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:434: ApprovalApi.fetchApprovals() /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:452: ApprovalPollJobService::class.java /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:465:private fun ApprovalInboxScreen( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:466: requestedApprovalId: String?, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:472: var approvals by remember { mutableStateOf>(emptyList()) } /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:473: var selectedId by remember { mutableStateOf(requestedApprovalId) } /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:478: LaunchedEffect(refreshKey, requestedApprovalId) { /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:483: ApprovalApi.fetchApprovals() /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:487: val requested = requestedApprovalId /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:493: ApprovalNotifications.notifyPending(context, loaded) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:495: error = it.message ?: "Unable to reach SHiRE Approval Hub" /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:505: .background(ApprovalBackground) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:520: "Private Tailscale channel", /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:521: color = ApprovalAmber, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:551: containerColor = ApprovalRed.copy(alpha = 0.16f) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:557: color = ApprovalRed /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:566: colors = CardDefaults.cardColors(containerColor = ApprovalSurface) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:572: color = ApprovalGreen /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:577: color = ApprovalMuted /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:591: ApprovalList( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:599: ApprovalDetails( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:608: ApprovalApi.submitDecision( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:617: error = it.message /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:625: ApprovalApi.createFileLink( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:638: error = it.message /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:649: ApprovalList( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:657: ApprovalDetails( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:666: ApprovalApi.submitDecision( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:675: error = it.message /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:683: ApprovalApi.createFileLink( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:696: error = it.message /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:708:private fun ApprovalList( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:709: approvals: List, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:716: colors = CardDefaults.cardColors(containerColor = ApprovalSurface), /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:732: ApprovalPurple.copy(alpha = 0.20f) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:734: ApprovalRaised /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:736: contentColor = ApprovalText /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:760:private fun ApprovalDetails( /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:761: approval: LiveApproval?, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:764: onOpenFile: (LiveApprovalFile) -> Unit /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:768: colors = CardDefaults.cardColors(containerColor = ApprovalSurface), /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:775: color = ApprovalMuted /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:801: Detail("STAGE", approval.stage, ApprovalAmber) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:802: Detail("WHY APPROVAL IS REQUIRED", approval.purpose, ApprovalText) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:803: Detail("WHAT IT AFFECTS", approval.affects, ApprovalBlue) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:804: Detail("RISK", approval.risk, ApprovalRed) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:805: Detail("ESTIMATED REVIEW", approval.estimate, ApprovalGreen) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:806: Detail("ROLLBACK", approval.rollback, ApprovalMuted) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:807: Detail("CREATED", approval.createdAt, ApprovalMuted) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:808: Detail("REPORT SHA-256", approval.reportSha256, ApprovalMuted) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:812: color = ApprovalMuted, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:823: color = ApprovalRed, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:828: Text("• $it", fontSize = 12.sp, color = ApprovalRed) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:838: color = ApprovalGreen /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:845: colors = CardDefaults.cardColors(containerColor = ApprovalRaised) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:851: color = ApprovalMuted, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:856: color = ApprovalMuted, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:875: Detail("RECORDED NOTE", it, ApprovalText) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:894: containerColor = ApprovalGreen, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:925: Text("REJECT", color = ApprovalRed) /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:931: "Approval is explicit and applies only to this exact " + /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:933: color = ApprovalAmber, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:949: color = ApprovalMuted, /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:964: "Approved" -> ApprovalGreen /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:965: "Rejected" -> ApprovalRed /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:966: "Changes Requested" -> ApprovalAmber /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:967: "Deferred" -> ApprovalBlue /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt:968: else -> ApprovalAmber /home/ray/ARMOR/apps/shire-mobile/app/src/main/AndroidManifest.xml:34: android:name=".ApprovalInboxActivity" /home/ray/ARMOR/apps/shire-mobile/app/src/main/AndroidManifest.xml:50: android:name=".ApprovalPollJobService" ============================================================ MOBILE KOTLIN SOURCES ============================================================ FILE: /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/ApprovalInboxActivity.kt 1 package com.shire.mobile 2 3 import android.Manifest 4 import android.app.Activity 5 import android.app.job.JobInfo 6 import android.app.job.JobParameters 7 import android.app.job.JobScheduler 8 import android.app.job.JobService 9 import android.app.NotificationChannel 10 import android.app.NotificationManager 11 import android.app.PendingIntent 12 import android.content.ComponentName 13 import android.content.Context 14 import android.content.Intent 15 import android.content.pm.PackageManager 16 import android.net.Uri 17 import android.os.Build 18 import android.os.Bundle 19 import androidx.activity.ComponentActivity 20 import androidx.activity.compose.setContent 21 import androidx.activity.result.contract.ActivityResultContracts 22 import androidx.compose.foundation.background 23 import androidx.compose.foundation.layout.Arrangement 24 import androidx.compose.foundation.layout.BoxWithConstraints 25 import androidx.compose.foundation.layout.Column 26 import androidx.compose.foundation.layout.Row 27 import androidx.compose.foundation.layout.Spacer 28 import androidx.compose.foundation.layout.fillMaxHeight 29 import androidx.compose.foundation.layout.fillMaxSize 30 import androidx.compose.foundation.layout.fillMaxWidth 31 import androidx.compose.foundation.layout.height 32 import androidx.compose.foundation.layout.heightIn 33 import androidx.compose.foundation.layout.padding 34 import androidx.compose.foundation.layout.widthIn 35 import androidx.compose.foundation.lazy.LazyColumn 36 import androidx.compose.foundation.lazy.items 37 import androidx.compose.foundation.rememberScrollState 38 import androidx.compose.foundation.shape.RoundedCornerShape 39 import androidx.compose.foundation.verticalScroll 40 import androidx.compose.material3.Button 41 import androidx.compose.material3.ButtonDefaults 42 import androidx.compose.material3.Card 43 import androidx.compose.material3.CardDefaults 44 import androidx.compose.material3.CircularProgressIndicator 45 import androidx.compose.material3.HorizontalDivider 46 import androidx.compose.material3.MaterialTheme 47 import androidx.compose.material3.OutlinedButton 48 import androidx.compose.material3.OutlinedTextField 49 import androidx.compose.material3.Surface 50 import androidx.compose.material3.Text 51 import androidx.compose.material3.TextButton 52 import androidx.compose.material3.darkColorScheme 53 import androidx.compose.runtime.Composable 54 import androidx.compose.runtime.LaunchedEffect 55 import androidx.compose.runtime.getValue 56 import androidx.compose.runtime.mutableIntStateOf 57 import androidx.compose.runtime.mutableStateOf 58 import androidx.compose.runtime.remember 59 import androidx.compose.runtime.rememberCoroutineScope 60 import androidx.compose.runtime.setValue 61 import androidx.compose.ui.Alignment 62 import androidx.compose.ui.Modifier 63 import androidx.compose.ui.graphics.Color 64 import androidx.compose.ui.platform.LocalContext 65 import androidx.compose.ui.text.font.FontWeight 66 import androidx.compose.ui.unit.dp 67 import androidx.compose.ui.unit.sp 68 import kotlinx.coroutines.Dispatchers 69 import kotlinx.coroutines.launch 70 import kotlinx.coroutines.withContext 71 import org.json.JSONArray 72 import org.json.JSONObject 73 import java.net.HttpURLConnection 74 import java.net.URI 75 import java.net.URL 76 import java.nio.charset.StandardCharsets 77 import java.util.Locale 78 79 private val ApprovalBackground = Color(0xFF090B10) 80 private val ApprovalSurface = Color(0xFF141923) 81 private val ApprovalRaised = Color(0xFF1B2330) 82 private val ApprovalPurple = Color(0xFFB174FF) 83 private val ApprovalBlue = Color(0xFF53B7FF) 84 private val ApprovalGreen = Color(0xFF20E887) 85 private val ApprovalAmber = Color(0xFFF2B84B) 86 private val ApprovalRed = Color(0xFFFF5A67) 87 private val ApprovalText = Color(0xFFE8FFF2) 88 private val ApprovalMuted = Color(0xFF91A0AA) 89 90 internal data class LiveApprovalFile( 91 val id: String, 92 val name: String, 93 val sizeHuman: String, 94 val sha256: String, 95 val mimeType: String, 96 val isEvidenceReport: Boolean 97 ) 98 99 internal data class LiveApproval( 100 val id: String, 101 val title: String, 102 val stage: String, 103 val purpose: String, 104 val affects: String, 105 val risk: String, 106 val urgency: String, 107 val estimate: String, 108 val rollback: String, 109 val status: String, 110 val createdAt: String, 111 val evidenceRoot: String, 112 val reportSha256: String, 113 val decisionNote: String?, 114 val permissionsRequested: List, 115 val permissionsNotGranted: List, 116 val files: List 117 ) 118 119 class ApprovalInboxActivity : ComponentActivity() { 120 private val notificationPermission = 121 registerForActivityResult(ActivityResultContracts.RequestPermission()) {} 122 123 override fun onCreate(savedInstanceState: Bundle?) { 124 super.onCreate(savedInstanceState) 125 126 ApprovalNotifications.ensureChannel(this) 127 ApprovalPollJobService.schedule(this) 128 ApprovalNotifications.checkNow(this) 129 130 if ( 131 Build.VERSION.SDK_INT >= 33 && 132 checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != 133 PackageManager.PERMISSION_GRANTED 134 ) { 135 notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) 136 } 137 138 val requestedApprovalId = intent 139 ?.data 140 ?.lastPathSegment 141 ?.takeIf { it.startsWith("APR-") } 142 143 setContent { 144 MaterialTheme( 145 colorScheme = darkColorScheme( 146 primary = ApprovalPurple, 147 secondary = ApprovalBlue, 148 background = ApprovalBackground, 149 surface = ApprovalSurface, 150 onPrimary = Color.Black, 151 onBackground = ApprovalText, 152 onSurface = ApprovalText 153 ) 154 ) { 155 Surface( 156 modifier = Modifier.fillMaxSize(), 157 color = ApprovalBackground 158 ) { 159 ApprovalInboxScreen( 160 requestedApprovalId = requestedApprovalId, 161 onClose = { finish() } 162 ) 163 } 164 } 165 } 166 } 167 } 168 169 internal object ApprovalApi { 170 private val baseUrl = BuildConfig.APPROVAL_API_BASE_URL.trimEnd('/') 171 private val token = BuildConfig.APPROVAL_API_TOKEN 172 173 private fun validatePrivateEndpoint() { 174 val uri = URI(baseUrl) 175 require(uri.scheme == "http") { 176 "Approval Hub must use the private Tailscale HTTP endpoint" 177 } 178 val host = uri.host ?: error("Approval Hub host is missing") 179 require(isTailnetAddress(host)) { 180 "Approval Hub refused a non-Tailscale endpoint" 181 } 182 require(token.length >= 32) { 183 "Approval Hub token is missing" 184 } 185 } 186 187 private fun isTailnetAddress(host: String): Boolean { 188 val parts = host.split(".") 189 if (parts.size == 4) { 190 val values = parts.mapNotNull { it.toIntOrNull() } 191 if (values.size == 4) { 192 return values[0] == 100 && values[1] in 64..127 193 } 194 } 195 return host.lowercase(Locale.ROOT).startsWith("fd7a:115c:a1e0:") 196 } 197 198 private fun request( 199 path: String, 200 method: String = "GET", 201 body: JSONObject? = null 202 ): JSONObject { 203 validatePrivateEndpoint() 204 val connection = URL(baseUrl + path).openConnection() as HttpURLConnection 205 try { 206 connection.requestMethod = method 207 connection.connectTimeout = 8_000 208 connection.readTimeout = 30_000 209 connection.setRequestProperty("Accept", "application/json") 210 connection.setRequestProperty("Authorization", "Bearer $token") 211 connection.setRequestProperty("Cache-Control", "no-store") 212 213 if (body != null) { 214 val bytes = body.toString().toByteArray(StandardCharsets.UTF_8) 215 connection.doOutput = true 216 connection.setRequestProperty("Content-Type", "application/json") 217 connection.setFixedLengthStreamingMode(bytes.size) 218 connection.outputStream.use { it.write(bytes) } 219 } 220 221 val code = connection.responseCode 222 val stream = if (code in 200..299) { 223 connection.inputStream 224 } else { 225 connection.errorStream 226 } 227 val text = stream 228 ?.bufferedReader(StandardCharsets.UTF_8) 229 ?.use { it.readText() } 230 .orEmpty() 231 val payload = if (text.isBlank()) JSONObject() else JSONObject(text) 232 if (code !in 200..299 || !payload.optBoolean("ok", false)) { 233 error(payload.optString("error", "Approval Hub HTTP $code")) 234 } 235 return payload 236 } finally { 237 connection.disconnect() 238 } 239 } 240 241 fun fetchApprovals(): List { 242 val rows = request("/api/v1/approvals").optJSONArray("approvals") 243 ?: JSONArray() 244 return buildList { 245 for (index in 0 until rows.length()) { 246 add(parseApproval(rows.getJSONObject(index))) 247 } 248 } 249 } 250 251 fun submitDecision( 252 approvalId: String, 253 decision: String, 254 note: String 255 ): LiveApproval { 256 val body = JSONObject() 257 .put("decision", decision) 258 .put("note", note) 259 .put("decided_by", "Ray via SHiRE Mobile") 260 val response = request( 261 "/api/v1/approvals/$approvalId/decision", 262 method = "POST", 263 body = body 264 ) 265 return parseApproval(response.getJSONObject("approval")) 266 } 267 268 fun createFileLink( 269 approvalId: String, 270 fileId: String 271 ): String { 272 val body = JSONObject().put("lifetime_seconds", 600) 273 return request( 274 "/api/v1/approvals/$approvalId/files/$fileId/link", 275 method = "POST", 276 body = body 277 ).getString("url") 278 } 279 280 private fun parseApproval(row: JSONObject): LiveApproval { 281 return LiveApproval( 282 id = row.getString("id"), 283 title = row.optString("title", "Approval required"), 284 stage = row.optString("stage", "Human review"), 285 purpose = row.optString("purpose"), 286 affects = row.optString("affects"), 287 risk = row.optString("risk"), 288 urgency = row.optString("urgency", "Normal"), 289 estimate = row.optString("estimate"), 290 rollback = row.optString("rollback"), 291 status = row.optString("status", "Pending"), 292 createdAt = row.optString("created_at"), 293 evidenceRoot = row.optString("evidence_root"), 294 reportSha256 = row.optString("report_sha256"), 295 decisionNote = row.optString("decision_note").takeIf { it.isNotBlank() }, 296 permissionsRequested = row.optJSONArray("permissions_requested") 297 .toStringList(), 298 permissionsNotGranted = row.optJSONArray("permissions_not_granted") 299 .toStringList(), 300 files = row.optJSONArray("files").toFileList() 301 ) 302 } 303 304 private fun JSONArray?.toStringList(): List { 305 if (this == null) return emptyList() 306 return buildList { 307 for (index in 0 until length()) { 308 add(optString(index)) 309 } 310 } 311 } 312 313 private fun JSONArray?.toFileList(): List { 314 if (this == null) return emptyList() 315 return buildList { 316 for (index in 0 until length()) { 317 val file = getJSONObject(index) 318 add( 319 LiveApprovalFile( 320 id = file.getString("id"), 321 name = file.optString("name"), 322 sizeHuman = file.optString("size_human"), 323 sha256 = file.optString("sha256"), 324 mimeType = file.optString( 325 "mime_type", 326 "application/octet-stream" 327 ), 328 isEvidenceReport = file.optBoolean( 329 "is_evidence_report", 330 false 331 ) 332 ) 333 ) 334 } 335 } 336 } 337 } 338 339 internal object ApprovalNotifications { 340 private const val CHANNEL_ID = "shire_approval_requests" 341 private const val CHANNEL_NAME = "SHiRE Approvals" 342 343 fun ensureChannel(context: Context) { 344 val manager = context.getSystemService(NotificationManager::class.java) 345 val channel = NotificationChannel( 346 CHANNEL_ID, 347 CHANNEL_NAME, 348 NotificationManager.IMPORTANCE_HIGH 349 ).apply { 350 description = "Private SHiRE approval requests requiring Ray's review" 351 } 352 manager.createNotificationChannel(channel) 353 } 354 355 fun checkNow(context: Context) { 356 Thread { 357 runCatching { 358 notifyPending(context, ApprovalApi.fetchApprovals()) 359 } 360 }.start() 361 } 362 363 fun notifyPending( 364 context: Context, 365 approvals: List 366 ) { 367 ensureChannel(context) 368 if ( 369 Build.VERSION.SDK_INT >= 33 && 370 context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != 371 PackageManager.PERMISSION_GRANTED 372 ) { 373 return 374 } 375 376 val preferences = context.getSharedPreferences( 377 "shire_approval_notifications", 378 Context.MODE_PRIVATE 379 ) 380 val manager = context.getSystemService(NotificationManager::class.java) 381 382 approvals 383 .filter { it.status == "Pending" } 384 .forEach { approval -> 385 if (preferences.getBoolean(approval.id, false)) return@forEach 386 387 val deepLink = Uri.parse("shire://approvals/${approval.id}") 388 val intent = Intent( 389 Intent.ACTION_VIEW, 390 deepLink, 391 context, 392 ApprovalInboxActivity::class.java 393 ) 394 val pendingIntent = PendingIntent.getActivity( 395 context, 396 approval.id.hashCode(), 397 intent, 398 PendingIntent.FLAG_UPDATE_CURRENT or 399 PendingIntent.FLAG_IMMUTABLE 400 ) 401 402 val notification = android.app.Notification.Builder( 403 context, 404 CHANNEL_ID 405 ) 406 .setSmallIcon(R.drawable.shire_icon_monochrome) 407 .setContentTitle("Approval required: ${approval.title}") 408 .setContentText( 409 "SHiRE has files and evidence ready for your review." 410 ) 411 .setStyle( 412 android.app.Notification.BigTextStyle().bigText( 413 "${approval.stage}. Tap to inspect the private " + 414 "evidence package and record your decision." 415 ) 416 ) 417 .setContentIntent(pendingIntent) 418 .setAutoCancel(true) 419 .setCategory(android.app.Notification.CATEGORY_STATUS) 420 .build() 421 422 manager.notify(approval.id.hashCode(), notification) 423 preferences.edit().putBoolean(approval.id, true).apply() 424 } 425 } 426 } 427 428 class ApprovalPollJobService : JobService() { 429 override fun onStartJob(params: JobParameters): Boolean { 430 Thread { 431 try { 432 ApprovalNotifications.notifyPending( 433 this, 434 ApprovalApi.fetchApprovals() 435 ) 436 } finally { 437 jobFinished(params, false) 438 } 439 }.start() 440 return true 441 } 442 443 override fun onStopJob(params: JobParameters): Boolean = true 444 445 companion object { 446 private const val JOB_ID = 0x53484952 447 448 fun schedule(context: Context) { 449 val scheduler = context.getSystemService(JobScheduler::class.java) 450 val component = ComponentName( 451 context, 452 ApprovalPollJobService::class.java 453 ) 454 val job = JobInfo.Builder(JOB_ID, component) 455 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) 456 .setPeriodic(15 * 60 * 1000L) 457 .setPersisted(true) 458 .build() 459 scheduler.schedule(job) 460 } 461 } 462 } 463 464 @Composable 465 private fun ApprovalInboxScreen( 466 requestedApprovalId: String?, 467 onClose: () -> Unit 468 ) { 469 val context = LocalContext.current 470 val scope = rememberCoroutineScope() 471 472 var approvals by remember { mutableStateOf>(emptyList()) } 473 var selectedId by remember { mutableStateOf(requestedApprovalId) } 474 var loading by remember { mutableStateOf(true) } 475 var error by remember { mutableStateOf(null) } 476 var refreshKey by remember { mutableIntStateOf(0) } 477 478 LaunchedEffect(refreshKey, requestedApprovalId) { 479 loading = true 480 error = null 481 runCatching { 482 withContext(Dispatchers.IO) { 483 ApprovalApi.fetchApprovals() 484 } 485 }.onSuccess { loaded -> 486 approvals = loaded 487 val requested = requestedApprovalId 488 ?.takeIf { id -> loaded.any { it.id == id } } 489 selectedId = requested 490 ?: selectedId?.takeIf { id -> loaded.any { it.id == id } } 491 ?: loaded.firstOrNull { it.status == "Pending" }?.id 492 ?: loaded.firstOrNull()?.id 493 ApprovalNotifications.notifyPending(context, loaded) 494 }.onFailure { 495 error = it.message ?: "Unable to reach SHiRE Approval Hub" 496 } 497 loading = false 498 } 499 500 val selected = approvals.firstOrNull { it.id == selectedId } 501 502 Column( 503 modifier = Modifier 504 .fillMaxSize() 505 .background(ApprovalBackground) 506 .padding(16.dp) 507 ) { 508 Row( 509 modifier = Modifier.fillMaxWidth(), 510 verticalAlignment = Alignment.CenterVertically 511 ) { 512 Column(modifier = Modifier.weight(1f)) { 513 Text( 514 "SHiRE APPROVAL INBOX", 515 fontSize = 23.sp, 516 fontWeight = FontWeight.Black 517 ) 518 Text( 519 "${approvals.count { it.status == "Pending" }} pending • " + 520 "Private Tailscale channel", 521 color = ApprovalAmber, 522 fontSize = 12.sp 523 ) 524 } 525 TextButton(onClick = { refreshKey += 1 }) { 526 Text("REFRESH") 527 } 528 TextButton(onClick = onClose) { 529 Text("CLOSE") 530 } 531 } 532 533 Spacer(Modifier.height(12.dp)) 534 535 if (loading && approvals.isEmpty()) { 536 Column( 537 modifier = Modifier.fillMaxSize(), 538 horizontalAlignment = Alignment.CenterHorizontally, 539 verticalArrangement = Arrangement.Center 540 ) { 541 CircularProgressIndicator() 542 Spacer(Modifier.height(12.dp)) 543 Text("Loading private approval packages…") 544 } 545 return@Column 546 } 547 548 error?.let { 549 Card( 550 colors = CardDefaults.cardColors( 551 containerColor = ApprovalRed.copy(alpha = 0.16f) 552 ) 553 ) { 554 Text( 555 it, 556 modifier = Modifier.padding(14.dp), 557 color = ApprovalRed 558 ) 559 } 560 Spacer(Modifier.height(10.dp)) 561 } 562 563 if (approvals.isEmpty()) { 564 Card( 565 modifier = Modifier.fillMaxWidth(), 566 colors = CardDefaults.cardColors(containerColor = ApprovalSurface) 567 ) { 568 Column(Modifier.padding(18.dp)) { 569 Text( 570 "NO APPROVALS WAITING", 571 fontWeight = FontWeight.Black, 572 color = ApprovalGreen 573 ) 574 Text( 575 "SHiRE will notify you when a real human-review " + 576 "boundary is reached.", 577 color = ApprovalMuted 578 ) 579 } 580 } 581 return@Column 582 } 583 584 BoxWithConstraints(modifier = Modifier.fillMaxSize()) { 585 val expanded = maxWidth >= 760.dp 586 if (expanded) { 587 Row( 588 modifier = Modifier.fillMaxSize(), 589 horizontalArrangement = Arrangement.spacedBy(14.dp) 590 ) { 591 ApprovalList( 592 approvals = approvals, 593 selectedId = selectedId, 594 onSelect = { selectedId = it }, 595 modifier = Modifier 596 .weight(0.82f) 597 .fillMaxHeight() 598 ) 599 ApprovalDetails( 600 approval = selected, 601 modifier = Modifier 602 .weight(1.18f) 603 .fillMaxHeight(), 604 onDecision = { decision, note -> 605 scope.launch { 606 runCatching { 607 withContext(Dispatchers.IO) { 608 ApprovalApi.submitDecision( 609 selected!!.id, 610 decision, 611 note 612 ) 613 } 614 }.onSuccess { 615 refreshKey += 1 616 }.onFailure { 617 error = it.message 618 } 619 } 620 }, 621 onOpenFile = { file -> 622 scope.launch { 623 runCatching { 624 withContext(Dispatchers.IO) { 625 ApprovalApi.createFileLink( 626 selected!!.id, 627 file.id 628 ) 629 } 630 }.onSuccess { url -> 631 context.startActivity( 632 Intent( 633 Intent.ACTION_VIEW, 634 Uri.parse(url) 635 ) 636 ) 637 }.onFailure { 638 error = it.message 639 } 640 } 641 } 642 ) 643 } 644 } else { 645 Column( 646 modifier = Modifier.fillMaxSize(), 647 verticalArrangement = Arrangement.spacedBy(12.dp) 648 ) { 649 ApprovalList( 650 approvals = approvals, 651 selectedId = selectedId, 652 onSelect = { selectedId = it }, 653 modifier = Modifier 654 .fillMaxWidth() 655 .heightIn(max = 250.dp) 656 ) 657 ApprovalDetails( 658 approval = selected, 659 modifier = Modifier 660 .fillMaxWidth() 661 .weight(1f), 662 onDecision = { decision, note -> 663 scope.launch { 664 runCatching { 665 withContext(Dispatchers.IO) { 666 ApprovalApi.submitDecision( 667 selected!!.id, 668 decision, 669 note 670 ) 671 } 672 }.onSuccess { 673 refreshKey += 1 674 }.onFailure { 675 error = it.message 676 } 677 } 678 }, 679 onOpenFile = { file -> 680 scope.launch { 681 runCatching { 682 withContext(Dispatchers.IO) { 683 ApprovalApi.createFileLink( 684 selected!!.id, 685 file.id 686 ) 687 } 688 }.onSuccess { url -> 689 context.startActivity( 690 Intent( 691 Intent.ACTION_VIEW, 692 Uri.parse(url) 693 ) 694 ) 695 }.onFailure { 696 error = it.message 697 } 698 } 699 } 700 ) 701 } 702 } 703 } 704 } 705 } 706 707 @Composable 708 private fun ApprovalList( 709 approvals: List, 710 selectedId: String?, 711 onSelect: (String) -> Unit, 712 modifier: Modifier = Modifier 713 ) { 714 Card( 715 modifier = modifier, 716 colors = CardDefaults.cardColors(containerColor = ApprovalSurface), 717 shape = RoundedCornerShape(18.dp) 718 ) { 719 LazyColumn( 720 modifier = Modifier 721 .fillMaxSize() 722 .padding(10.dp), 723 verticalArrangement = Arrangement.spacedBy(8.dp) 724 ) { 725 items(approvals, key = { it.id }) { approval -> 726 val selected = approval.id == selectedId 727 OutlinedButton( 728 onClick = { onSelect(approval.id) }, 729 modifier = Modifier.fillMaxWidth(), 730 colors = ButtonDefaults.outlinedButtonColors( 731 containerColor = if (selected) { 732 ApprovalPurple.copy(alpha = 0.20f) 733 } else { 734 ApprovalRaised 735 }, 736 contentColor = ApprovalText 737 ) 738 ) { 739 Column( 740 modifier = Modifier.fillMaxWidth(), 741 horizontalAlignment = Alignment.Start 742 ) { 743 Text( 744 approval.title, 745 fontWeight = FontWeight.Bold 746 ) 747 Text( 748 "${approval.stage} • ${approval.status}", 749 color = statusColor(approval.status), 750 fontSize = 11.sp 751 ) 752 } 753 } 754 } 755 } 756 } 757 } 758 759 @Composable 760 private fun ApprovalDetails( 761 approval: LiveApproval?, 762 modifier: Modifier = Modifier, 763 onDecision: (String, String) -> Unit, 764 onOpenFile: (LiveApprovalFile) -> Unit 765 ) { 766 Card( 767 modifier = modifier, 768 colors = CardDefaults.cardColors(containerColor = ApprovalSurface), 769 shape = RoundedCornerShape(18.dp) 770 ) { 771 if (approval == null) { 772 Text( 773 "Select an approval package.", 774 modifier = Modifier.padding(18.dp), 775 color = ApprovalMuted 776 ) 777 return@Card 778 } 779 780 var reviewNote by remember(approval.id) { mutableStateOf("") } 781 782 Column( 783 modifier = Modifier 784 .fillMaxWidth() 785 .fillMaxHeight() 786 .verticalScroll(rememberScrollState()) 787 .padding(18.dp) 788 ) { 789 Text( 790 approval.title, 791 fontSize = 21.sp, 792 fontWeight = FontWeight.Black 793 ) 794 Text( 795 "${approval.status} • ${approval.urgency} urgency", 796 color = statusColor(approval.status), 797 fontWeight = FontWeight.Bold 798 ) 799 800 Spacer(Modifier.height(12.dp)) 801 Detail("STAGE", approval.stage, ApprovalAmber) 802 Detail("WHY APPROVAL IS REQUIRED", approval.purpose, ApprovalText) 803 Detail("WHAT IT AFFECTS", approval.affects, ApprovalBlue) 804 Detail("RISK", approval.risk, ApprovalRed) 805 Detail("ESTIMATED REVIEW", approval.estimate, ApprovalGreen) 806 Detail("ROLLBACK", approval.rollback, ApprovalMuted) 807 Detail("CREATED", approval.createdAt, ApprovalMuted) 808 Detail("REPORT SHA-256", approval.reportSha256, ApprovalMuted) 809 810 Text( 811 "REQUESTED PERMISSIONS", 812 color = ApprovalMuted, 813 fontSize = 10.sp, 814 fontWeight = FontWeight.Black 815 ) 816 approval.permissionsRequested.forEach { 817 Text("• $it", fontSize = 12.sp) 818 } 819 820 Spacer(Modifier.height(8.dp)) 821 Text( 822 "NOT GRANTED BY THIS APPROVAL", 823 color = ApprovalRed, 824 fontSize = 10.sp, 825 fontWeight = FontWeight.Black 826 ) 827 approval.permissionsNotGranted.forEach { 828 Text("• $it", fontSize = 12.sp, color = ApprovalRed) 829 } 830 831 Spacer(Modifier.height(12.dp)) 832 HorizontalDivider() 833 Spacer(Modifier.height(12.dp)) 834 835 Text( 836 "FILES & EVIDENCE (${approval.files.size})", 837 fontWeight = FontWeight.Black, 838 color = ApprovalGreen 839 ) 840 approval.files.forEach { file -> 841 Card( 842 modifier = Modifier 843 .fillMaxWidth() 844 .padding(top = 7.dp), 845 colors = CardDefaults.cardColors(containerColor = ApprovalRaised) 846 ) { 847 Column(Modifier.padding(12.dp)) { 848 Text(file.name, fontWeight = FontWeight.Bold) 849 Text( 850 "${file.sizeHuman} • ${file.mimeType}", 851 color = ApprovalMuted, 852 fontSize = 11.sp 853 ) 854 Text( 855 "SHA-256 ${file.sha256}", 856 color = ApprovalMuted, 857 fontSize = 9.sp, 858 lineHeight = 12.sp 859 ) 860 TextButton(onClick = { onOpenFile(file) }) { 861 Text( 862 if (file.isEvidenceReport) { 863 "OPEN EVIDENCE" 864 } else { 865 "OPEN / SAVE COPY" 866 } 867 ) 868 } 869 } 870 } 871 } 872 873 approval.decisionNote?.let { 874 Spacer(Modifier.height(12.dp)) 875 Detail("RECORDED NOTE", it, ApprovalText) 876 } 877 878 if (approval.status == "Pending") { 879 Spacer(Modifier.height(14.dp)) 880 OutlinedTextField( 881 value = reviewNote, 882 onValueChange = { reviewNote = it.take(2000) }, 883 modifier = Modifier.fillMaxWidth(), 884 label = { Text("Review note or requested changes") }, 885 minLines = 2, 886 maxLines = 5 887 ) 888 Spacer(Modifier.height(10.dp)) 889 890 Button( 891 onClick = { onDecision("approve", reviewNote) }, 892 modifier = Modifier.fillMaxWidth(), 893 colors = ButtonDefaults.buttonColors( 894 containerColor = ApprovalGreen, 895 contentColor = Color.Black 896 ) 897 ) { 898 Text("APPROVE THIS EVIDENCE", fontWeight = FontWeight.Black) 899 } 900 901 Spacer(Modifier.height(7.dp)) 902 OutlinedButton( 903 onClick = { 904 onDecision("request_changes", reviewNote) 905 }, 906 modifier = Modifier.fillMaxWidth() 907 ) { 908 Text("REQUEST CHANGES") 909 } 910 911 Row( 912 modifier = Modifier.fillMaxWidth(), 913 horizontalArrangement = Arrangement.spacedBy(7.dp) 914 ) { 915 OutlinedButton( 916 onClick = { onDecision("defer", reviewNote) }, 917 modifier = Modifier.weight(1f) 918 ) { 919 Text("DEFER") 920 } 921 OutlinedButton( 922 onClick = { onDecision("reject", reviewNote) }, 923 modifier = Modifier.weight(1f) 924 ) { 925 Text("REJECT", color = ApprovalRed) 926 } 927 } 928 929 Spacer(Modifier.height(8.dp)) 930 Text( 931 "Approval is explicit and applies only to this exact " + 932 "evidence package. Forge and printing remain locked.", 933 color = ApprovalAmber, 934 fontSize = 11.sp 935 ) 936 } 937 } 938 } 939 } 940 941 @Composable 942 private fun Detail( 943 label: String, 944 value: String, 945 color: Color 946 ) { 947 Text( 948 label, 949 color = ApprovalMuted, 950 fontSize = 10.sp, 951 fontWeight = FontWeight.Black 952 ) 953 Text( 954 value.ifBlank { "Not supplied" }, 955 color = color, 956 fontSize = 13.sp, 957 lineHeight = 18.sp 958 ) 959 Spacer(Modifier.height(9.dp)) 960 } 961 962 private fun statusColor(status: String): Color { 963 return when (status) { 964 "Approved" -> ApprovalGreen 965 "Rejected" -> ApprovalRed 966 "Changes Requested" -> ApprovalAmber 967 "Deferred" -> ApprovalBlue 968 else -> ApprovalAmber 969 } 970 } FILE: /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt 1 package com.shire.mobile 2 3 import android.content.Intent 4 import android.os.Bundle 5 import android.provider.Settings 6 import androidx.activity.ComponentActivity 7 import androidx.activity.compose.setContent 8 import androidx.compose.animation.AnimatedContent 9 import androidx.compose.animation.AnimatedVisibility 10 import androidx.compose.animation.core.Animatable 11 import androidx.compose.animation.core.RepeatMode 12 import androidx.compose.animation.core.animateFloat 13 import androidx.compose.animation.core.infiniteRepeatable 14 import androidx.compose.animation.core.rememberInfiniteTransition 15 import androidx.compose.animation.core.tween 16 import androidx.compose.foundation.Image 17 import androidx.compose.foundation.background 18 import androidx.compose.foundation.clickable 19 import androidx.compose.foundation.layout.Arrangement 20 import androidx.compose.foundation.layout.Box 21 import androidx.compose.foundation.layout.BoxWithConstraints 22 import androidx.compose.foundation.layout.Column 23 import androidx.compose.foundation.layout.FlowRow 24 import androidx.compose.foundation.layout.Row 25 import androidx.compose.foundation.layout.Spacer 26 import androidx.compose.foundation.layout.fillMaxHeight 27 import androidx.compose.foundation.layout.fillMaxSize 28 import androidx.compose.foundation.layout.fillMaxWidth 29 import androidx.compose.foundation.layout.height 30 import androidx.compose.foundation.layout.heightIn 31 import androidx.compose.foundation.layout.offset 32 import androidx.compose.foundation.layout.padding 33 import androidx.compose.foundation.layout.size 34 import androidx.compose.foundation.layout.width 35 import androidx.compose.foundation.layout.widthIn 36 import androidx.compose.foundation.rememberScrollState 37 import androidx.compose.foundation.shape.CircleShape 38 import androidx.compose.foundation.shape.RoundedCornerShape 39 import androidx.compose.foundation.verticalScroll 40 import androidx.compose.material3.AlertDialog 41 import androidx.compose.material3.Button 42 import androidx.compose.material3.ButtonDefaults 43 import androidx.compose.material3.Card 44 import androidx.compose.material3.CardDefaults 45 import androidx.compose.material3.ExperimentalMaterial3Api 46 import androidx.compose.material3.FilterChip 47 import androidx.compose.material3.LinearProgressIndicator 48 import androidx.compose.material3.MaterialTheme 49 import androidx.compose.material3.OutlinedButton 50 import androidx.compose.material3.OutlinedTextField 51 import androidx.compose.material3.RadioButton 52 import androidx.compose.material3.Scaffold 53 import androidx.compose.material3.Switch 54 import androidx.compose.material3.Text 55 import androidx.compose.material3.TextButton 56 import androidx.compose.material3.darkColorScheme 57 import androidx.compose.runtime.Composable 58 import androidx.compose.runtime.LaunchedEffect 59 import androidx.compose.runtime.getValue 60 import androidx.compose.runtime.mutableIntStateOf 61 import androidx.compose.runtime.mutableStateOf 62 import androidx.compose.runtime.remember 63 import androidx.compose.runtime.setValue 64 import androidx.compose.ui.Alignment 65 import androidx.compose.ui.Modifier 66 import androidx.compose.ui.draw.clip 67 import androidx.compose.ui.graphics.Color 68 import androidx.compose.ui.graphics.graphicsLayer 69 import androidx.compose.ui.layout.ContentScale 70 import androidx.compose.ui.platform.LocalContext 71 import androidx.compose.ui.res.painterResource 72 import androidx.compose.ui.text.font.FontWeight 73 import androidx.compose.ui.text.style.TextAlign 74 import androidx.compose.ui.unit.dp 75 import androidx.compose.ui.unit.sp 76 import kotlinx.coroutines.delay 77 78 private val Background = Color(0xFF090B10) 79 private val Surface = Color(0xFF11151D) 80 private val Raised = Color(0xFF181D28) 81 private val RaisedSoft = Color(0xFF202634) 82 private val Purple = Color(0xFFA987FF) 83 private val Blue = Color(0xFF73D7FF) 84 private val Green = Color(0xFF75E6A4) 85 private val Amber = Color(0xFFFFC766) 86 private val Red = Color(0xFFFF7D86) 87 private val TextPrimary = Color(0xFFF2F3F7) 88 private val TextMuted = Color(0xFF9EA6B5) 89 90 private enum class Screen { 91 Home, 92 Academy 93 } 94 95 private enum class ShireExpression { 96 Normal, 97 Happy, 98 Thinking, 99 Sentinel, 100 Concerned 101 } 102 103 private enum class ShireMode { 104 Calm, 105 Academy, 106 Sentinel, 107 Forge 108 } 109 110 private data class Skill( 111 val id: String, 112 val name: String, 113 val category: String, 114 val stage: String, 115 val progress: Float, 116 val state: String, 117 val evidence: Int, 118 val result: String, 119 val forge: String 120 ) 121 122 private data class ApprovalItem( 123 val id: Int, 124 val title: String, 125 val stage: String, 126 val purpose: String, 127 val affects: String, 128 val risk: String, 129 val estimate: String, 130 val rollback: String, 131 val status: String = "Pending" 132 ) 133 134 class MainActivity : ComponentActivity() { 135 override fun onCreate(savedInstanceState: Bundle?) { 136 super.onCreate(savedInstanceState) 137 138 // SHIRE-MOBILE-APPROVAL-INBOX-0001 139 ApprovalNotifications.ensureChannel(this) 140 ApprovalPollJobService.schedule(this) 141 ApprovalNotifications.checkNow(this) 142 143 setContent { 144 MaterialTheme( 145 colorScheme = darkColorScheme( 146 primary = Purple, 147 secondary = Blue, 148 background = Background, 149 surface = Surface, 150 onPrimary = Color.Black, 151 onBackground = TextPrimary, 152 onSurface = TextPrimary 153 ) 154 ) { 155 ShireMobileApp() 156 } 157 } 158 } 159 } 160 161 @Composable 162 private fun ShireMobileApp() { 163 val context = LocalContext.current 164 165 val systemAnimationsEnabled = remember { 166 Settings.Global.getFloat( 167 context.contentResolver, 168 Settings.Global.ANIMATOR_DURATION_SCALE, 169 1f 170 ) > 0f 171 } 172 173 var personalityAnimations by remember { 174 mutableStateOf(systemAnimationsEnabled) 175 } 176 177 var screen by remember { mutableStateOf(Screen.Home) } 178 var mode by remember { mutableStateOf(ShireMode.Calm) } 179 var expression by remember { mutableStateOf(ShireExpression.Normal) } 180 var showLearnPrint by remember { mutableStateOf(false) } 181 var showApprovals by remember { mutableStateOf(false) } 182 var message by remember { 183 mutableStateOf("Systems ready, Chief. What are we building today?") 184 } 185 186 val handleModeChange: (ShireMode) -> Unit = { selected -> 187 mode = selected 188 189 when (selected) { 190 ShireMode.Calm -> { 191 screen = Screen.Home 192 expression = ShireExpression.Normal 193 message = "Calm command mode ready, Chief." 194 } 195 196 ShireMode.Academy -> { 197 screen = Screen.Academy 198 expression = ShireExpression.Thinking 199 message = "Academy mode ready. Let us review what I have learned." 200 } 201 202 ShireMode.Sentinel -> { 203 screen = Screen.Home 204 expression = ShireExpression.Sentinel 205 message = "Sentinel mode active. Security systems have priority." 206 } 207 208 ShireMode.Forge -> { 209 screen = Screen.Home 210 expression = ShireExpression.Happy 211 message = "Forge mode ready. Let us build something remarkable." 212 } 213 } 214 } 215 216 var approvals by remember { 217 mutableStateOf(emptyList()) 218 } 219 220 BoxWithConstraints( 221 modifier = Modifier 222 .fillMaxSize() 223 .background(Background) 224 ) { 225 val expanded = maxWidth >= 700.dp 226 227 Scaffold(containerColor = Background) { paddingValues -> 228 if (expanded) { 229 FoldCommandCentre( 230 modifier = Modifier.padding(paddingValues), 231 screen = screen, 232 mode = mode, 233 expression = expression, 234 message = message, 235 approvals = approvals, 236 personalityAnimations = personalityAnimations, 237 onPersonalityAnimationsChange = { 238 personalityAnimations = it 239 }, 240 onScreenChange = { screen = it }, 241 onModeChange = handleModeChange, 242 onExpressionChange = { expression = it }, 243 onMessageChange = { message = it }, 244 onLearnPrint = { 245 expression = ShireExpression.Thinking 246 showLearnPrint = true 247 }, 248 onApprovals = { 249 expression = ShireExpression.Thinking 250 context.startActivity( 251 Intent( 252 context, 253 ApprovalInboxActivity::class.java 254 ) 255 ) 256 }, 257 onAddRecommendation = { 258 approvals = approvals + recommendedApproval(approvals) 259 expression = ShireExpression.Happy 260 message = "Learning recommendation added for approval." 261 } 262 ) 263 } else { 264 FoldedCompanion( 265 modifier = Modifier.padding(paddingValues), 266 screen = screen, 267 mode = mode, 268 expression = expression, 269 message = message, 270 approvals = approvals, 271 personalityAnimations = personalityAnimations, 272 onScreenChange = { screen = it }, 273 onModeChange = handleModeChange, 274 onExpressionChange = { expression = it }, 275 onMessageChange = { message = it }, 276 onLearnPrint = { 277 expression = ShireExpression.Thinking 278 showLearnPrint = true 279 }, 280 onApprovals = { 281 expression = ShireExpression.Thinking 282 context.startActivity( 283 Intent( 284 context, 285 ApprovalInboxActivity::class.java 286 ) 287 ) 288 }, 289 onAddRecommendation = { 290 approvals = approvals + recommendedApproval(approvals) 291 expression = ShireExpression.Happy 292 message = "Learning recommendation added for approval." 293 } 294 ) 295 } 296 } 297 } 298 299 if (showLearnPrint) { 300 LearnAndPrintDialog( 301 onDismiss = { 302 showLearnPrint = false 303 expression = ShireExpression.Normal 304 }, 305 onCreatePlan = { request, mode -> 306 approvals = approvals + ApprovalItem( 307 id = (approvals.maxOfOrNull { it.id } ?: 0) + 1, 308 title = request.take(55), 309 stage = "Approve Learning", 310 purpose = request, 311 affects = "Creates a Safe Plan only. No learning, design, slicing or printing starts.", 312 risk = "Awaiting safety, licensing and feasibility analysis.", 313 estimate = mode, 314 rollback = "Fully cancellable before staged approval." 315 ) 316 317 showLearnPrint = false 318 expression = ShireExpression.Happy 319 message = "Safe Plan added to the Approval Centre." 320 } 321 ) 322 } 323 324 if (showApprovals) { 325 ApprovalCentreDialog( 326 approvals = approvals, 327 onDismiss = { 328 showApprovals = false 329 expression = ShireExpression.Normal 330 }, 331 onDecision = { id, decision -> 332 approvals = approvals.map { 333 if (it.id == id) it.copy(status = decision) else it 334 } 335 336 expression = when (decision) { 337 "Approved" -> ShireExpression.Happy 338 "Rejected" -> ShireExpression.Concerned 339 "Changes Requested" -> ShireExpression.Thinking 340 else -> ShireExpression.Normal 341 } 342 343 message = when (decision) { 344 "Approved" -> "Only that exact stage was approved." 345 "Rejected" -> "Plan rejected. No action will be taken." 346 "Deferred" -> "Plan deferred and remains inactive." 347 "Changes Requested" -> "A revised plan is now required." 348 else -> "Approval updated." 349 } 350 } 351 ) 352 } 353 } 354 355 private fun recommendedApproval( 356 approvals: List 357 ): ApprovalItem { 358 return ApprovalItem( 359 id = (approvals.maxOfOrNull { it.id } ?: 0) + 1, 360 title = "Parametric phone support geometry", 361 stage = "Approve Learning", 362 purpose = "Learn reusable phone-fit and support geometry for DockForge and Forge.", 363 affects = "Adds an Academy Safe Plan only.", 364 risk = "Low. No certification or Forge-permission changes.", 365 estimate = "45–90 minutes.", 366 rollback = "Can be rejected, deferred or cancelled." 367 ) 368 } 369 370 @Composable 371 private fun FoldedCompanion( 372 modifier: Modifier, 373 screen: Screen, 374 mode: ShireMode, 375 expression: ShireExpression, 376 message: String, 377 approvals: List, 378 personalityAnimations: Boolean, 379 onScreenChange: (Screen) -> Unit, 380 onModeChange: (ShireMode) -> Unit, 381 onExpressionChange: (ShireExpression) -> Unit, 382 onMessageChange: (String) -> Unit, 383 onLearnPrint: () -> Unit, 384 onApprovals: () -> Unit, 385 onAddRecommendation: () -> Unit 386 ) { 387 Column( 388 modifier = modifier 389 .fillMaxSize() 390 .padding(horizontal = 17.dp, vertical = 13.dp), 391 horizontalAlignment = Alignment.CenterHorizontally 392 ) { 393 AppHeader(compact = true) 394 395 Spacer(modifier = Modifier.height(9.dp)) 396 397 ModeSelector( 398 selected = mode, 399 onSelect = onModeChange 400 ) 401 402 Spacer(modifier = Modifier.height(10.dp)) 403 404 Box( 405 modifier = Modifier 406 .fillMaxWidth() 407 .weight(1f), 408 contentAlignment = Alignment.TopCenter 409 ) { 410 when (screen) { 411 Screen.Home -> { 412 Column( 413 modifier = Modifier 414 .fillMaxSize() 415 .verticalScroll(rememberScrollState()), 416 horizontalAlignment = Alignment.CenterHorizontally 417 ) { 418 RealShireFace( 419 expression = expression, 420 expanded = false, 421 personalityAnimations = personalityAnimations 422 ) 423 424 Spacer(modifier = Modifier.height(11.dp)) 425 426 Text( 427 text = message, 428 textAlign = TextAlign.Center, 429 fontSize = 16.sp, 430 lineHeight = 22.sp 431 ) 432 433 Spacer(modifier = Modifier.height(14.dp)) 434 435 CompactStatus( 436 pendingApprovals = approvals.count { 437 it.status == "Pending" 438 } 439 ) 440 441 Spacer(modifier = Modifier.height(12.dp)) 442 443 MainActionPanel( 444 pendingApprovals = approvals.count { 445 it.status == "Pending" 446 }, 447 onLearnPrint = onLearnPrint, 448 onApprovals = onApprovals, 449 onAcademy = { 450 onScreenChange(Screen.Academy) 451 }, 452 onShire = { 453 onExpressionChange( 454 ShireExpression.Normal 455 ) 456 onMessageChange( 457 "Local SHiRE mode selected." 458 ) 459 }, 460 onChatGpt = { 461 onExpressionChange( 462 ShireExpression.Thinking 463 ) 464 onMessageChange( 465 "ChatGPT mode selected." 466 ) 467 }, 468 onBoth = { 469 onExpressionChange( 470 ShireExpression.Happy 471 ) 472 onMessageChange( 473 "SHiRE and ChatGPT collaboration selected." 474 ) 475 } 476 ) 477 478 Spacer(modifier = Modifier.height(14.dp)) 479 480 CurrentLearningCard() 481 482 Spacer(modifier = Modifier.height(12.dp)) 483 } 484 } 485 486 Screen.Academy -> { 487 MiniAcademy( 488 expanded = false, 489 onBack = { 490 onScreenChange(Screen.Home) 491 }, 492 onApprovals = onApprovals, 493 onAddRecommendation = onAddRecommendation 494 ) 495 } 496 } 497 } 498 } 499 } 500 501 @Composable 502 private fun FoldCommandCentre( 503 modifier: Modifier, 504 screen: Screen, 505 mode: ShireMode, 506 expression: ShireExpression, 507 message: String, 508 approvals: List, 509 personalityAnimations: Boolean, 510 onPersonalityAnimationsChange: (Boolean) -> Unit, 511 onScreenChange: (Screen) -> Unit, 512 onModeChange: (ShireMode) -> Unit, 513 onExpressionChange: (ShireExpression) -> Unit, 514 onMessageChange: (String) -> Unit, 515 onLearnPrint: () -> Unit, 516 onApprovals: () -> Unit, 517 onAddRecommendation: () -> Unit 518 ) { 519 Row( 520 modifier = modifier 521 .fillMaxSize() 522 .padding(18.dp), 523 horizontalArrangement = Arrangement.spacedBy(16.dp) 524 ) { 525 Column( 526 modifier = Modifier 527 .weight(0.92f) 528 .fillMaxHeight() 529 .verticalScroll(rememberScrollState()), 530 horizontalAlignment = Alignment.CenterHorizontally 531 ) { 532 AppHeader(compact = false) 533 534 Spacer(modifier = Modifier.height(9.dp)) 535 536 ModeSelector( 537 selected = mode, 538 onSelect = onModeChange 539 ) 540 541 Spacer(modifier = Modifier.height(8.dp)) 542 543 RealShireFace( 544 expression = expression, 545 expanded = true, 546 personalityAnimations = personalityAnimations 547 ) 548 549 Spacer(modifier = Modifier.height(10.dp)) 550 551 Text( 552 text = message, 553 modifier = Modifier.widthIn(max = 460.dp), 554 textAlign = TextAlign.Center, 555 fontSize = 18.sp, 556 lineHeight = 24.sp 557 ) 558 559 Spacer(modifier = Modifier.height(13.dp)) 560 561 CurrentMissionCard() 562 563 Spacer(modifier = Modifier.height(12.dp)) 564 565 MainActionPanel( 566 pendingApprovals = approvals.count { it.status == "Pending" }, 567 onLearnPrint = onLearnPrint, 568 onApprovals = onApprovals, 569 onAcademy = { onScreenChange(Screen.Academy) }, 570 onShire = { 571 onExpressionChange(ShireExpression.Normal) 572 onMessageChange("Local SHiRE mode selected.") 573 }, 574 onChatGpt = { 575 onExpressionChange(ShireExpression.Thinking) 576 onMessageChange("ChatGPT mode selected.") 577 }, 578 onBoth = { 579 onExpressionChange(ShireExpression.Happy) 580 onMessageChange("SHiRE and ChatGPT collaboration selected.") 581 } 582 ) 583 584 Spacer(modifier = Modifier.height(12.dp)) 585 586 PersonalityAnimationControl( 587 enabled = personalityAnimations, 588 onChange = onPersonalityAnimationsChange 589 ) 590 } 591 592 Card( 593 modifier = Modifier 594 .weight(1.35f) 595 .fillMaxHeight(), 596 colors = CardDefaults.cardColors(containerColor = Surface), 597 shape = RoundedCornerShape(24.dp) 598 ) { 599 when (screen) { 600 Screen.Home -> { 601 CommandCentreOverview( 602 mode = mode, 603 approvals = approvals, 604 onAcademy = { onScreenChange(Screen.Academy) }, 605 onApprovals = onApprovals, 606 onAddRecommendation = onAddRecommendation 607 ) 608 } 609 610 Screen.Academy -> { 611 MiniAcademy( 612 expanded = true, 613 onBack = { onScreenChange(Screen.Home) }, 614 onApprovals = onApprovals, 615 onAddRecommendation = onAddRecommendation 616 ) 617 } 618 } 619 } 620 } 621 } 622 623 624 @Composable 625 private fun ModeSelector( 626 selected: ShireMode, 627 onSelect: (ShireMode) -> Unit 628 ) { 629 Card( 630 modifier = Modifier 631 .fillMaxWidth() 632 .widthIn(max = 650.dp), 633 colors = CardDefaults.cardColors(containerColor = Surface), 634 shape = RoundedCornerShape(18.dp) 635 ) { 636 FlowRow( 637 modifier = Modifier.padding(9.dp), 638 horizontalArrangement = Arrangement.spacedBy(7.dp), 639 verticalArrangement = Arrangement.spacedBy(7.dp) 640 ) { 641 ShireMode.entries.forEach { mode -> 642 FilterChip( 643 selected = selected == mode, 644 onClick = { onSelect(mode) }, 645 label = { 646 Text( 647 when (mode) { 648 ShireMode.Calm -> "Calm" 649 ShireMode.Academy -> "Academy" 650 ShireMode.Sentinel -> "Sentinel" 651 ShireMode.Forge -> "Forge" 652 } 653 ) 654 } 655 ) 656 } 657 } 658 } 659 } 660 661 @Composable 662 private fun ModeWorkspace(mode: ShireMode) { 663 val title: String 664 val subtitle: String 665 val status: String 666 val accent: Color 667 val details: List> 668 669 when (mode) { 670 ShireMode.Calm -> { 671 title = "Calm Command" 672 subtitle = "Daily assistance, conversation and mission control." 673 status = "Ready" 674 accent = Green 675 details = listOf( 676 "Conversation" to "Local SHiRE ready", 677 "Current mission" to "Mobile command expansion", 678 "Private routing" to "Tailscale only" 679 ) 680 } 681 682 ShireMode.Academy -> { 683 title = "Academy" 684 subtitle = "Learning, evidence, curriculum and recommendations." 685 status = "Learning" 686 accent = Blue 687 details = listOf( 688 "Current skill" to "Boolean Operations", 689 "Progress" to "72%", 690 "Protected skills" to "371" 691 ) 692 } 693 694 ShireMode.Sentinel -> { 695 title = "Sentinel" 696 subtitle = "Security posture, network alerts and protected actions." 697 status = "Protected" 698 accent = Red 699 details = listOf( 700 "Threat posture" to "Monitoring", 701 "Network control" to "Approval required", 702 "Public ports" to "None" 703 ) 704 } 705 706 ShireMode.Forge -> { 707 title = "Forge" 708 subtitle = "Design, modelling, slicing and printer readiness." 709 status = "Ready" 710 accent = Purple 711 details = listOf( 712 "Design engine" to "CadQuery available", 713 "Print actions" to "Approval locked", 714 "Default material" to "PLA" 715 ) 716 } 717 } 718 719 Card( 720 modifier = Modifier.fillMaxWidth(), 721 colors = CardDefaults.cardColors(containerColor = Raised), 722 shape = RoundedCornerShape(20.dp) 723 ) { 724 Column(modifier = Modifier.padding(17.dp)) { 725 Row(verticalAlignment = Alignment.CenterVertically) { 726 Column(modifier = Modifier.weight(1f)) { 727 SectionLabel("ACTIVE SHIRE MODE") 728 729 Text( 730 text = title, 731 fontSize = 21.sp, 732 fontWeight = FontWeight.Black 733 ) 734 } 735 736 Text( 737 text = status, 738 color = accent, 739 fontWeight = FontWeight.Bold 740 ) 741 } 742 743 Text( 744 text = subtitle, 745 color = TextMuted, 746 lineHeight = 19.sp 747 ) 748 749 Spacer(modifier = Modifier.height(11.dp)) 750 751 details.forEach { (label, value) -> 752 Row(modifier = Modifier.padding(vertical = 3.dp)) { 753 Text( 754 text = label, 755 modifier = Modifier.weight(1f), 756 color = TextMuted 757 ) 758 759 Text( 760 text = value, 761 color = accent, 762 fontWeight = FontWeight.SemiBold 763 ) 764 } 765 } 766 } 767 } 768 } 769 770 @Composable 771 private fun AppHeader(compact: Boolean) { 772 Column(horizontalAlignment = Alignment.CenterHorizontally) { 773 Text( 774 text = "SHiRE", 775 fontSize = if (compact) 27.sp else 31.sp, 776 fontWeight = FontWeight.Black, 777 letterSpacing = 5.sp 778 ) 779 780 Text( 781 text = if (compact) "MOBILE COMMAND" else "FOLD COMMAND CENTRE", 782 color = TextMuted, 783 fontSize = 10.sp, 784 letterSpacing = 2.sp 785 ) 786 } 787 } 788 789 @Composable 790 private fun RealShireFace( 791 expression: ShireExpression, 792 expanded: Boolean, 793 personalityAnimations: Boolean 794 ) { 795 val transition = rememberInfiniteTransition(label = "shire-idle") 796 797 val breathing by transition.animateFloat( 798 initialValue = if (personalityAnimations) 0.988f else 1f, 799 targetValue = if (personalityAnimations) 1.014f else 1f, 800 animationSpec = infiniteRepeatable( 801 animation = tween(2400), 802 repeatMode = RepeatMode.Reverse 803 ), 804 label = "breathing" 805 ) 806 807 val floating by transition.animateFloat( 808 initialValue = if (personalityAnimations) -2f else 0f, 809 targetValue = if (personalityAnimations) 3f else 0f, 810 animationSpec = infiniteRepeatable( 811 animation = tween(3000), 812 repeatMode = RepeatMode.Reverse 813 ), 814 label = "floating" 815 ) 816 817 val stretchX = remember { Animatable(1f) } 818 val stretchY = remember { Animatable(1f) } 819 var greetingVisible by remember { mutableStateOf(false) } 820 821 LaunchedEffect(expanded, personalityAnimations) { 822 if (expanded && personalityAnimations) { 823 greetingVisible = true 824 825 stretchX.snapTo(0.92f) 826 stretchY.snapTo(1.05f) 827 828 stretchX.animateTo( 829 targetValue = 1.10f, 830 animationSpec = tween(320) 831 ) 832 stretchY.animateTo( 833 targetValue = 0.94f, 834 animationSpec = tween(320) 835 ) 836 837 stretchX.animateTo( 838 targetValue = 1f, 839 animationSpec = tween(350) 840 ) 841 stretchY.animateTo( 842 targetValue = 1f, 843 animationSpec = tween(350) 844 ) 845 846 delay(1100) 847 greetingVisible = false 848 } else { 849 stretchX.snapTo(1f) 850 stretchY.snapTo(1f) 851 greetingVisible = false 852 } 853 } 854 855 val drawable = when (expression) { 856 ShireExpression.Normal -> R.drawable.shire_face_main 857 ShireExpression.Happy -> R.drawable.shire_face_happy 858 ShireExpression.Thinking -> R.drawable.shire_face_thinking 859 ShireExpression.Sentinel -> R.drawable.shire_face_sentinel 860 ShireExpression.Concerned -> R.drawable.shire_face_scared 861 } 862 863 Column(horizontalAlignment = Alignment.CenterHorizontally) { 864 Card( 865 modifier = Modifier 866 .size(if (expanded) 330.dp else 238.dp) 867 .offset(y = floating.dp) 868 .graphicsLayer { 869 scaleX = breathing * stretchX.value 870 scaleY = breathing * stretchY.value 871 }, 872 shape = CircleShape, 873 colors = CardDefaults.cardColors(containerColor = Raised), 874 elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) 875 ) { 876 AnimatedContent( 877 targetState = drawable, 878 label = "shire-expression" 879 ) { face -> 880 Image( 881 painter = painterResource(face), 882 contentDescription = "SHiRE", 883 modifier = Modifier 884 .fillMaxSize() 885 .padding(4.dp) 886 .clip(CircleShape), 887 contentScale = ContentScale.Crop 888 ) 889 } 890 } 891 892 AnimatedVisibility(visible = greetingVisible) { 893 Text( 894 text = "Ahh… much better, Chief.", 895 modifier = Modifier.padding(top = 8.dp), 896 color = Green, 897 fontWeight = FontWeight.SemiBold 898 ) 899 } 900 } 901 } 902 903 @Composable 904 private fun CompactStatus(pendingApprovals: Int) { 905 FlowRow( 906 modifier = Modifier.fillMaxWidth(), 907 horizontalArrangement = Arrangement.spacedBy(7.dp), 908 verticalArrangement = Arrangement.spacedBy(7.dp) 909 ) { 910 StatusChip("ACADEMY", "Running", Green) 911 StatusChip("APPROVALS", "Open inbox", Amber) 912 StatusChip("SENTINEL", "Protected", Green) 913 } 914 } 915 916 @Composable 917 private fun StatusChip( 918 title: String, 919 value: String, 920 valueColor: Color 921 ) { 922 Card( 923 colors = CardDefaults.cardColors(containerColor = Raised), 924 shape = RoundedCornerShape(13.dp) 925 ) { 926 Column( 927 modifier = Modifier.padding( 928 horizontal = 12.dp, 929 vertical = 9.dp 930 ) 931 ) { 932 Text( 933 text = title, 934 color = TextMuted, 935 fontSize = 9.sp, 936 fontWeight = FontWeight.Bold 937 ) 938 939 Text( 940 text = value, 941 color = valueColor, 942 fontSize = 13.sp, 943 fontWeight = FontWeight.SemiBold 944 ) 945 } 946 } 947 } 948 949 @Composable 950 private fun MainActionPanel( 951 pendingApprovals: Int, 952 onLearnPrint: () -> Unit, 953 onApprovals: () -> Unit, 954 onAcademy: () -> Unit, 955 onShire: () -> Unit, 956 onChatGpt: () -> Unit, 957 onBoth: () -> Unit 958 ) { 959 Card( 960 modifier = Modifier 961 .fillMaxWidth() 962 .widthIn(max = 650.dp), 963 colors = CardDefaults.cardColors(containerColor = Surface), 964 shape = RoundedCornerShape(20.dp) 965 ) { 966 Column( 967 modifier = Modifier.padding(14.dp), 968 verticalArrangement = Arrangement.spacedBy(9.dp) 969 ) { 970 Button( 971 onClick = onLearnPrint, 972 modifier = Modifier 973 .fillMaxWidth() 974 .height(52.dp), 975 colors = ButtonDefaults.buttonColors( 976 containerColor = Purple, 977 contentColor = Color.Black 978 ) 979 ) { 980 Text("LEARN & PRINT", fontWeight = FontWeight.Black) 981 } 982 983 Row( 984 horizontalArrangement = Arrangement.spacedBy(8.dp) 985 ) { 986 Button( 987 onClick = onAcademy, 988 modifier = Modifier.weight(1f), 989 colors = ButtonDefaults.buttonColors( 990 containerColor = Blue, 991 contentColor = Color.Black 992 ) 993 ) { 994 Text("ACADEMY", fontWeight = FontWeight.Bold) 995 } 996 997 Button( 998 onClick = onApprovals, 999 modifier = Modifier.weight(1f), 1000 colors = ButtonDefaults.buttonColors( 1001 containerColor = Amber, 1002 contentColor = Color.Black 1003 ) 1004 ) { 1005 Text("APPROVALS") 1006 } 1007 } 1008 1009 Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) { 1010 OutlinedButton( 1011 onClick = onShire, 1012 modifier = Modifier.weight(1f) 1013 ) { 1014 Text("SHiRE") 1015 } 1016 1017 OutlinedButton( 1018 onClick = onChatGpt, 1019 modifier = Modifier.weight(1f) 1020 ) { 1021 Text("ChatGPT") 1022 } 1023 1024 OutlinedButton( 1025 onClick = onBoth, 1026 modifier = Modifier.weight(1f) 1027 ) { 1028 Text("Both") 1029 } 1030 } 1031 } 1032 } 1033 } 1034 1035 @Composable 1036 private fun CurrentMissionCard() { 1037 Card( 1038 modifier = Modifier.fillMaxWidth(), 1039 colors = CardDefaults.cardColors(containerColor = Surface), 1040 shape = RoundedCornerShape(18.dp) 1041 ) { 1042 Column(modifier = Modifier.padding(14.dp)) { 1043 SectionLabel("CURRENT MISSION") 1044 1045 Text( 1046 text = "Expand SHiRE Mobile capability", 1047 fontWeight = FontWeight.Bold, 1048 fontSize = 17.sp 1049 ) 1050 1051 Text( 1052 text = "Mini Academy and adaptive Fold command centre", 1053 color = TextMuted, 1054 fontSize = 13.sp 1055 ) 1056 1057 Spacer(modifier = Modifier.height(8.dp)) 1058 1059 LinearProgressIndicator( 1060 progress = { 0.72f }, 1061 modifier = Modifier.fillMaxWidth() 1062 ) 1063 } 1064 } 1065 } 1066 1067 @Composable 1068 private fun CurrentLearningCard() { 1069 Card( 1070 modifier = Modifier.fillMaxWidth(), 1071 colors = CardDefaults.cardColors(containerColor = Surface), 1072 shape = RoundedCornerShape(18.dp) 1073 ) { 1074 Column(modifier = Modifier.padding(15.dp)) { 1075 SectionLabel("CURRENT LEARNING") 1076 1077 Text( 1078 text = "Boolean Operations", 1079 fontWeight = FontWeight.Bold, 1080 fontSize = 17.sp 1081 ) 1082 1083 Text( 1084 text = "Geometry Foundations • Stage 3", 1085 color = TextMuted 1086 ) 1087 1088 Spacer(modifier = Modifier.height(8.dp)) 1089 1090 LinearProgressIndicator( 1091 progress = { 0.72f }, 1092 modifier = Modifier.fillMaxWidth() 1093 ) 1094 1095 Spacer(modifier = Modifier.height(6.dp)) 1096 1097 Text( 1098 text = "72% • Read-only mobile view", 1099 color = Green, 1100 fontSize = 12.sp 1101 ) 1102 } 1103 } 1104 } 1105 1106 @Composable 1107 private fun PersonalityAnimationControl( 1108 enabled: Boolean, 1109 onChange: (Boolean) -> Unit 1110 ) { 1111 Card( 1112 modifier = Modifier.fillMaxWidth(), 1113 colors = CardDefaults.cardColors(containerColor = Surface), 1114 shape = RoundedCornerShape(17.dp) 1115 ) { 1116 Row( 1117 modifier = Modifier.padding(13.dp), 1118 verticalAlignment = Alignment.CenterVertically 1119 ) { 1120 Column(modifier = Modifier.weight(1f)) { 1121 Text( 1122 text = "Personality animations", 1123 fontWeight = FontWeight.SemiBold 1124 ) 1125 1126 Text( 1127 text = "Breathing, floating and unfolding stretch", 1128 color = TextMuted, 1129 fontSize = 12.sp 1130 ) 1131 } 1132 1133 Switch( 1134 checked = enabled, 1135 onCheckedChange = onChange 1136 ) 1137 } 1138 } 1139 } 1140 1141 @Composable 1142 private fun CommandCentreOverview( 1143 mode: ShireMode, 1144 approvals: List, 1145 onAcademy: () -> Unit, 1146 onApprovals: () -> Unit, 1147 onAddRecommendation: () -> Unit 1148 ) { 1149 Column( 1150 modifier = Modifier 1151 .fillMaxSize() 1152 .verticalScroll(rememberScrollState()) 1153 .padding(18.dp) 1154 ) { 1155 Text( 1156 text = "COMMAND OVERVIEW", 1157 fontSize = 24.sp, 1158 fontWeight = FontWeight.Black 1159 ) 1160 1161 Text( 1162 text = "SHiRE Mini • private Tailscale command workspace", 1163 color = TextMuted 1164 ) 1165 1166 Spacer(modifier = Modifier.height(15.dp)) 1167 1168 ModeWorkspace(mode) 1169 1170 Spacer(modifier = Modifier.height(14.dp)) 1171 1172 OverviewMetricRow( 1173 approvals = approvals.count { it.status == "Pending" } 1174 ) 1175 1176 Spacer(modifier = Modifier.height(14.dp)) 1177 1178 AcademySummaryCard(onOpen = onAcademy) 1179 1180 Spacer(modifier = Modifier.height(12.dp)) 1181 1182 ApprovalSummaryCard( 1183 approvals = approvals, 1184 onOpen = onApprovals 1185 ) 1186 1187 Spacer(modifier = Modifier.height(12.dp)) 1188 1189 RecommendationCard(onAdd = onAddRecommendation) 1190 1191 Spacer(modifier = Modifier.height(12.dp)) 1192 1193 SystemGuardrailsCard() 1194 } 1195 } 1196 1197 @Composable 1198 private fun OverviewMetricRow(approvals: Int) { 1199 FlowRow( 1200 horizontalArrangement = Arrangement.spacedBy(9.dp), 1201 verticalArrangement = Arrangement.spacedBy(9.dp) 1202 ) { 1203 MetricCard("371", "Skills") 1204 MetricCard("72%", "Current skill") 1205 MetricCard("$approvals", "Approvals") 1206 MetricCard("SAFE", "Guardrails") 1207 } 1208 } 1209 1210 @Composable 1211 private fun MetricCard(value: String, label: String) { 1212 Card( 1213 colors = CardDefaults.cardColors(containerColor = Raised), 1214 shape = RoundedCornerShape(15.dp) 1215 ) { 1216 Column( 1217 modifier = Modifier.padding( 1218 horizontal = 17.dp, 1219 vertical = 12.dp 1220 ) 1221 ) { 1222 Text( 1223 text = value, 1224 fontSize = 20.sp, 1225 fontWeight = FontWeight.Black, 1226 color = Purple 1227 ) 1228 1229 Text( 1230 text = label, 1231 color = TextMuted, 1232 fontSize = 11.sp 1233 ) 1234 } 1235 } 1236 } 1237 1238 @Composable 1239 private fun AcademySummaryCard(onOpen: () -> Unit) { 1240 Card( 1241 modifier = Modifier 1242 .fillMaxWidth() 1243 .clickable(onClick = onOpen), 1244 colors = CardDefaults.cardColors(containerColor = Raised), 1245 shape = RoundedCornerShape(18.dp) 1246 ) { 1247 Column(modifier = Modifier.padding(16.dp)) { 1248 SectionLabel("MINI ACADEMY") 1249 1250 Text( 1251 text = "Boolean Operations", 1252 fontSize = 19.sp, 1253 fontWeight = FontWeight.Bold 1254 ) 1255 1256 Text( 1257 text = "Current learning • Stage 3 • Running", 1258 color = Green 1259 ) 1260 1261 Spacer(modifier = Modifier.height(9.dp)) 1262 1263 LinearProgressIndicator( 1264 progress = { 0.72f }, 1265 modifier = Modifier.fillMaxWidth() 1266 ) 1267 1268 Spacer(modifier = Modifier.height(7.dp)) 1269 1270 Text( 1271 text = "Tap to inspect curriculum, queue, evidence and guardrails.", 1272 color = TextMuted, 1273 fontSize = 12.sp 1274 ) 1275 } 1276 } 1277 } 1278 1279 @Composable 1280 private fun ApprovalSummaryCard( 1281 approvals: List, 1282 onOpen: () -> Unit 1283 ) { 1284 Card( 1285 modifier = Modifier 1286 .fillMaxWidth() 1287 .clickable(onClick = onOpen), 1288 colors = CardDefaults.cardColors(containerColor = Raised), 1289 shape = RoundedCornerShape(18.dp) 1290 ) { 1291 Column(modifier = Modifier.padding(16.dp)) { 1292 SectionLabel("APPROVAL CENTRE") 1293 1294 Text( 1295 text = "${approvals.count { it.status == "Pending" }} actions waiting", 1296 fontSize = 18.sp, 1297 fontWeight = FontWeight.Bold, 1298 color = Amber 1299 ) 1300 1301 approvals 1302 .filter { it.status == "Pending" } 1303 .take(2) 1304 .forEach { 1305 Text( 1306 text = "• ${it.stage}: ${it.title}", 1307 color = TextMuted, 1308 fontSize = 13.sp 1309 ) 1310 } 1311 } 1312 } 1313 } 1314 1315 @Composable 1316 private fun MiniAcademy( 1317 expanded: Boolean, 1318 onBack: () -> Unit, 1319 onApprovals: () -> Unit, 1320 onAddRecommendation: () -> Unit 1321 ) { 1322 val skills = remember { 1323 listOf( 1324 Skill( 1325 id = "boolean", 1326 name = "Boolean Operations", 1327 category = "Geometry Foundations", 1328 stage = "Stage 3", 1329 progress = 0.72f, 1330 state = "Learning", 1331 evidence = 22, 1332 result = "Practice active", 1333 forge = "Locked" 1334 ), 1335 Skill( 1336 id = "parameters", 1337 name = "Parameter Architecture", 1338 category = "Parametric CAD", 1339 stage = "Review", 1340 progress = 1f, 1341 state = "Learned", 1342 evidence = 18, 1343 result = "Forge allowed", 1344 forge = "Allowed" 1345 ), 1346 Skill( 1347 id = "supports", 1348 name = "Printable Support Geometry", 1349 category = "Manufacturing", 1350 stage = "Queued", 1351 progress = 0.16f, 1352 state = "Queued", 1353 evidence = 3, 1354 result = "Awaiting plan", 1355 forge = "Locked" 1356 ) 1357 ) 1358 } 1359 1360 var selectedSkillId by remember { mutableStateOf(skills.first().id) } 1361 val selectedSkill = skills.first { it.id == selectedSkillId } 1362 1363 Column( 1364 modifier = Modifier 1365 .fillMaxSize() 1366 .verticalScroll(rememberScrollState()) 1367 .padding(if (expanded) 19.dp else 0.dp) 1368 ) { 1369 Row(verticalAlignment = Alignment.CenterVertically) { 1370 if (!expanded) { 1371 TextButton(onClick = onBack) { 1372 Text("Back") 1373 } 1374 } 1375 1376 Column(modifier = Modifier.weight(1f)) { 1377 Text( 1378 text = "MINI ACADEMY", 1379 fontSize = 25.sp, 1380 fontWeight = FontWeight.Black 1381 ) 1382 1383 Text( 1384 text = "Running • protected read-only view", 1385 color = Green, 1386 fontSize = 12.sp 1387 ) 1388 } 1389 1390 OutlinedButton(onClick = onApprovals) { 1391 Text("Approvals") 1392 } 1393 } 1394 1395 Spacer(modifier = Modifier.height(14.dp)) 1396 1397 AcademyHero() 1398 1399 Spacer(modifier = Modifier.height(13.dp)) 1400 1401 if (expanded) { 1402 Row( 1403 horizontalArrangement = Arrangement.spacedBy(12.dp) 1404 ) { 1405 Column(modifier = Modifier.weight(0.92f)) { 1406 SkillList( 1407 skills = skills, 1408 selectedSkillId = selectedSkillId, 1409 onSelect = { selectedSkillId = it } 1410 ) 1411 1412 Spacer(modifier = Modifier.height(12.dp)) 1413 1414 LearningQueueCard() 1415 } 1416 1417 Column(modifier = Modifier.weight(1.08f)) { 1418 SkillDetail(selectedSkill) 1419 1420 Spacer(modifier = Modifier.height(12.dp)) 1421 1422 RecommendationCard(onAdd = onAddRecommendation) 1423 1424 Spacer(modifier = Modifier.height(12.dp)) 1425 1426 SystemGuardrailsCard() 1427 } 1428 } 1429 } else { 1430 SkillList( 1431 skills = skills, 1432 selectedSkillId = selectedSkillId, 1433 onSelect = { selectedSkillId = it } 1434 ) 1435 1436 Spacer(modifier = Modifier.height(12.dp)) 1437 1438 SkillDetail(selectedSkill) 1439 1440 Spacer(modifier = Modifier.height(12.dp)) 1441 1442 LearningQueueCard() 1443 1444 Spacer(modifier = Modifier.height(12.dp)) 1445 1446 RecommendationCard(onAdd = onAddRecommendation) 1447 1448 Spacer(modifier = Modifier.height(12.dp)) 1449 1450 SystemGuardrailsCard() 1451 } 1452 } 1453 } 1454 1455 @Composable 1456 private fun AcademyHero() { 1457 Card( 1458 modifier = Modifier.fillMaxWidth(), 1459 colors = CardDefaults.cardColors(containerColor = Raised), 1460 shape = RoundedCornerShape(20.dp) 1461 ) { 1462 Column(modifier = Modifier.padding(17.dp)) { 1463 SectionLabel("CURRENT LEARNING") 1464 1465 Text( 1466 text = "Boolean Operations", 1467 fontSize = 21.sp, 1468 fontWeight = FontWeight.Black 1469 ) 1470 1471 Text( 1472 text = "Geometry Foundations • Stage 3", 1473 color = TextMuted 1474 ) 1475 1476 Spacer(modifier = Modifier.height(10.dp)) 1477 1478 LinearProgressIndicator( 1479 progress = { 0.72f }, 1480 modifier = Modifier.fillMaxWidth() 1481 ) 1482 1483 Spacer(modifier = Modifier.height(7.dp)) 1484 1485 Row { 1486 Text( 1487 text = "72% knowledge progress", 1488 color = Blue, 1489 modifier = Modifier.weight(1f) 1490 ) 1491 1492 Text( 1493 text = "22 successful runs", 1494 color = Green 1495 ) 1496 } 1497 1498 Spacer(modifier = Modifier.height(8.dp)) 1499 1500 Text( 1501 text = "The protected 371-skill state remains unchanged. " + 1502 "No certification or Forge permissions can be altered here.", 1503 color = TextMuted, 1504 fontSize = 12.sp, 1505 lineHeight = 17.sp 1506 ) 1507 } 1508 } 1509 } 1510 1511 @Composable 1512 private fun SkillList( 1513 skills: List, 1514 selectedSkillId: String, 1515 onSelect: (String) -> Unit 1516 ) { 1517 Card( 1518 modifier = Modifier.fillMaxWidth(), 1519 colors = CardDefaults.cardColors(containerColor = Raised), 1520 shape = RoundedCornerShape(19.dp) 1521 ) { 1522 Column(modifier = Modifier.padding(14.dp)) { 1523 SectionLabel("SKILLS") 1524 1525 skills.forEach { skill -> 1526 Card( 1527 modifier = Modifier 1528 .fillMaxWidth() 1529 .padding(vertical = 4.dp) 1530 .clickable { onSelect(skill.id) }, 1531 colors = CardDefaults.cardColors( 1532 containerColor = if (skill.id == selectedSkillId) { 1533 RaisedSoft 1534 } else { 1535 Surface 1536 } 1537 ), 1538 shape = RoundedCornerShape(14.dp) 1539 ) { 1540 Column(modifier = Modifier.padding(12.dp)) { 1541 Row { 1542 Column(modifier = Modifier.weight(1f)) { 1543 Text( 1544 text = skill.name, 1545 fontWeight = FontWeight.Bold 1546 ) 1547 1548 Text( 1549 text = "${skill.category} • ${skill.stage}", 1550 color = TextMuted, 1551 fontSize = 11.sp 1552 ) 1553 } 1554 1555 Text( 1556 text = skill.state, 1557 color = when (skill.state) { 1558 "Learned" -> Green 1559 "Learning" -> Blue 1560 else -> Amber 1561 }, 1562 fontSize = 12.sp 1563 ) 1564 } 1565 1566 Spacer(modifier = Modifier.height(6.dp)) 1567 1568 LinearProgressIndicator( 1569 progress = { skill.progress }, 1570 modifier = Modifier.fillMaxWidth() 1571 ) 1572 } 1573 } 1574 } 1575 } 1576 } 1577 } 1578 1579 @Composable 1580 private fun SkillDetail(skill: Skill) { 1581 Card( 1582 modifier = Modifier.fillMaxWidth(), 1583 colors = CardDefaults.cardColors(containerColor = Raised), 1584 shape = RoundedCornerShape(19.dp) 1585 ) { 1586 Column(modifier = Modifier.padding(16.dp)) { 1587 SectionLabel("SKILL DETAIL") 1588 1589 Text( 1590 text = skill.name, 1591 fontSize = 20.sp, 1592 fontWeight = FontWeight.Black 1593 ) 1594 1595 Text( 1596 text = "${skill.category} • ${skill.stage}", 1597 color = TextMuted 1598 ) 1599 1600 Spacer(modifier = Modifier.height(11.dp)) 1601 1602 DetailLine("State", skill.state) 1603 DetailLine("Progress", "${(skill.progress * 100).toInt()}%") 1604 DetailLine("Evidence records", skill.evidence.toString()) 1605 DetailLine("Current result", skill.result) 1606 DetailLine("Forge permission", skill.forge) 1607 1608 Spacer(modifier = Modifier.height(10.dp)) 1609 1610 Text( 1611 text = "Mobile Academy is read-only. Any change requires a Safe Plan and explicit approval.", 1612 color = Green, 1613 fontSize = 12.sp, 1614 lineHeight = 17.sp 1615 ) 1616 } 1617 } 1618 } 1619 1620 @Composable 1621 private fun LearningQueueCard() { 1622 Card( 1623 modifier = Modifier.fillMaxWidth(), 1624 colors = CardDefaults.cardColors(containerColor = Raised), 1625 shape = RoundedCornerShape(19.dp) 1626 ) { 1627 Column(modifier = Modifier.padding(16.dp)) { 1628 SectionLabel("LEARNING QUEUE") 1629 1630 QueueLine("Printable Support Geometry", "Next") 1631 QueueLine("Hard-Surface Modelling", "Planned") 1632 QueueLine("Blender Cloth Fundamentals", "Recommended") 1633 } 1634 } 1635 } 1636 1637 @Composable 1638 private fun QueueLine(name: String, state: String) { 1639 Row( 1640 modifier = Modifier.padding(vertical = 5.dp) 1641 ) { 1642 Text( 1643 text = name, 1644 modifier = Modifier.weight(1f), 1645 fontWeight = FontWeight.SemiBold 1646 ) 1647 1648 Text( 1649 text = state, 1650 color = Amber, 1651 fontSize = 12.sp 1652 ) 1653 } 1654 } 1655 1656 @Composable 1657 private fun RecommendationCard(onAdd: () -> Unit) { 1658 Card( 1659 modifier = Modifier.fillMaxWidth(), 1660 colors = CardDefaults.cardColors(containerColor = Raised), 1661 shape = RoundedCornerShape(19.dp) 1662 ) { 1663 Column(modifier = Modifier.padding(16.dp)) { 1664 SectionLabel("RECOMMENDED NEXT") 1665 1666 Text( 1667 text = "Parametric phone support geometry", 1668 fontSize = 18.sp, 1669 fontWeight = FontWeight.Bold 1670 ) 1671 1672 Text( 1673 text = "Supports DockForge, Forge accuracy and sellable products.", 1674 color = TextMuted, 1675 lineHeight = 19.sp 1676 ) 1677 1678 Spacer(modifier = Modifier.height(7.dp)) 1679 1680 Text( 1681 text = "Estimated effort: 45–90 minutes", 1682 color = Blue 1683 ) 1684 1685 Text( 1686 text = "Benefit: Forge + income", 1687 color = Green 1688 ) 1689 1690 Spacer(modifier = Modifier.height(10.dp)) 1691 1692 Button(onClick = onAdd) { 1693 Text("Add for Approval") 1694 } 1695 } 1696 } 1697 } 1698 1699 @Composable 1700 private fun SystemGuardrailsCard() { 1701 Card( 1702 modifier = Modifier.fillMaxWidth(), 1703 colors = CardDefaults.cardColors(containerColor = Raised), 1704 shape = RoundedCornerShape(19.dp) 1705 ) { 1706 Column(modifier = Modifier.padding(16.dp)) { 1707 SectionLabel("GUARDRAILS") 1708 1709 GuardrailLine("Existing 371-skill state", "Protected") 1710 GuardrailLine("Counted attempts", "Unchanged") 1711 GuardrailLine("Certification", "Approval required") 1712 GuardrailLine("Forge permissions", "Locked by default") 1713 GuardrailLine("Automatic printing", "Disabled") 1714 } 1715 } 1716 } 1717 1718 @Composable 1719 private fun GuardrailLine(name: String, state: String) { 1720 Row( 1721 modifier = Modifier.padding(vertical = 4.dp) 1722 ) { 1723 Text( 1724 text = name, 1725 modifier = Modifier.weight(1f), 1726 color = TextMuted 1727 ) 1728 1729 Text( 1730 text = state, 1731 color = Green, 1732 fontWeight = FontWeight.SemiBold, 1733 fontSize = 12.sp 1734 ) 1735 } 1736 } 1737 1738 @Composable 1739 private fun DetailLine(label: String, value: String) { 1740 Row( 1741 modifier = Modifier.padding(vertical = 4.dp) 1742 ) { 1743 Text( 1744 text = label, 1745 modifier = Modifier.weight(1f), 1746 color = TextMuted 1747 ) 1748 1749 Text( 1750 text = value, 1751 fontWeight = FontWeight.SemiBold 1752 ) 1753 } 1754 } 1755 1756 @Composable 1757 private fun SectionLabel(text: String) { 1758 Text( 1759 text = text, 1760 color = TextMuted, 1761 fontSize = 10.sp, 1762 fontWeight = FontWeight.Bold, 1763 letterSpacing = 1.4.sp 1764 ) 1765 1766 Spacer(modifier = Modifier.height(6.dp)) 1767 } 1768 1769 @OptIn(ExperimentalMaterial3Api::class) 1770 @Composable 1771 private fun LearnAndPrintDialog( 1772 onDismiss: () -> Unit, 1773 onCreatePlan: (String, String) -> Unit 1774 ) { 1775 val modes = listOf( 1776 "Learn Only", 1777 "Design Only", 1778 "Prepare to Print", 1779 "Learn + Design + Print" 1780 ) 1781 1782 var request by remember { mutableStateOf("") } 1783 var selectedMode by remember { 1784 mutableStateOf("Learn + Design + Print") 1785 } 1786 var printer by remember { mutableStateOf("Bambu P1S #1") } 1787 var material by remember { mutableStateOf("PLA") } 1788 1789 AlertDialog( 1790 onDismissRequest = onDismiss, 1791 title = { 1792 Column { 1793 Text( 1794 text = "Learn & Print", 1795 fontSize = 24.sp, 1796 fontWeight = FontWeight.Black 1797 ) 1798 1799 Text( 1800 text = "Create an approval-gated SHiRE mission.", 1801 color = TextMuted, 1802 fontSize = 13.sp 1803 ) 1804 } 1805 }, 1806 text = { 1807 Column( 1808 modifier = Modifier 1809 .heightIn(max = 580.dp) 1810 .verticalScroll(rememberScrollState()) 1811 ) { 1812 OutlinedTextField( 1813 value = request, 1814 onValueChange = { 1815 if (it.length <= 1000) request = it 1816 }, 1817 modifier = Modifier 1818 .fillMaxWidth() 1819 .heightIn(min = 155.dp), 1820 label = { Text("Describe the mission") }, 1821 supportingText = { 1822 Text( 1823 text = "${request.length} / 1000", 1824 modifier = Modifier.fillMaxWidth(), 1825 textAlign = TextAlign.End 1826 ) 1827 }, 1828 placeholder = { 1829 Text( 1830 "Learn how to design a gothic controller holder, " + 1831 "show me the model and prepare it for black PLA." 1832 ) 1833 }, 1834 maxLines = 12 1835 ) 1836 1837 Spacer(modifier = Modifier.height(10.dp)) 1838 SectionLabel("MISSION TYPE") 1839 1840 modes.forEach { mode -> 1841 Row(verticalAlignment = Alignment.CenterVertically) { 1842 RadioButton( 1843 selected = selectedMode == mode, 1844 onClick = { selectedMode = mode } 1845 ) 1846 Text(mode) 1847 } 1848 } 1849 1850 SectionLabel("PRINTER") 1851 1852 FlowRow( 1853 horizontalArrangement = Arrangement.spacedBy(7.dp) 1854 ) { 1855 listOf( 1856 "Bambu P1S #1", 1857 "Bambu P1S #2", 1858 "Creality K2 Plus", 1859 "Decide Later" 1860 ).forEach { 1861 FilterChip( 1862 selected = printer == it, 1863 onClick = { printer = it }, 1864 label = { Text(it) } 1865 ) 1866 } 1867 } 1868 1869 Spacer(modifier = Modifier.height(8.dp)) 1870 SectionLabel("MATERIAL") 1871 1872 FlowRow( 1873 horizontalArrangement = Arrangement.spacedBy(7.dp) 1874 ) { 1875 listOf( 1876 "PLA", 1877 "PETG", 1878 "TPU", 1879 "Decide Later" 1880 ).forEach { 1881 FilterChip( 1882 selected = material == it, 1883 onClick = { material = it }, 1884 label = { Text(it) } 1885 ) 1886 } 1887 } 1888 1889 Spacer(modifier = Modifier.height(10.dp)) 1890 1891 Card( 1892 colors = CardDefaults.cardColors(containerColor = Raised) 1893 ) { 1894 Text( 1895 text = "Submitting creates a Safe Plan only. Learning, design, " + 1896 "slicing and printing remain separately locked.", 1897 modifier = Modifier.padding(12.dp), 1898 color = Green, 1899 fontSize = 12.sp 1900 ) 1901 } 1902 } 1903 }, 1904 confirmButton = { 1905 Button( 1906 enabled = request.isNotBlank(), 1907 onClick = { 1908 onCreatePlan( 1909 request.trim(), 1910 "$selectedMode • $printer • $material" 1911 ) 1912 } 1913 ) { 1914 Text("Create Safe Plan") 1915 } 1916 }, 1917 dismissButton = { 1918 TextButton(onClick = onDismiss) { 1919 Text("Cancel") 1920 } 1921 }, 1922 containerColor = Surface 1923 ) 1924 } 1925 1926 @Composable 1927 private fun ApprovalCentreDialog( 1928 approvals: List, 1929 onDismiss: () -> Unit, 1930 onDecision: (Int, String) -> Unit 1931 ) { 1932 var selectedId by remember { 1933 mutableIntStateOf( 1934 approvals.firstOrNull { it.status == "Pending" }?.id 1935 ?: approvals.firstOrNull()?.id 1936 ?: -1 1937 ) 1938 } 1939 1940 val selected = approvals.firstOrNull { it.id == selectedId } 1941 1942 AlertDialog( 1943 onDismissRequest = onDismiss, 1944 title = { 1945 Column { 1946 Text( 1947 text = "Approval Centre", 1948 fontSize = 24.sp, 1949 fontWeight = FontWeight.Black 1950 ) 1951 1952 Text( 1953 text = "${approvals.count { it.status == "Pending" }} pending", 1954 color = Amber 1955 ) 1956 } 1957 }, 1958 text = { 1959 Column( 1960 modifier = Modifier 1961 .heightIn(max = 610.dp) 1962 .verticalScroll(rememberScrollState()) 1963 ) { 1964 approvals.forEach { item -> 1965 FilterChip( 1966 selected = selectedId == item.id, 1967 onClick = { selectedId = item.id }, 1968 label = { 1969 Text("${item.stage}: ${item.title} — ${item.status}") 1970 }, 1971 modifier = Modifier.fillMaxWidth() 1972 ) 1973 } 1974 1975 selected?.let { item -> 1976 Spacer(modifier = Modifier.height(12.dp)) 1977 1978 ApprovalDetail("STAGE", item.stage, Amber) 1979 ApprovalDetail("PURPOSE", item.purpose, TextPrimary) 1980 ApprovalDetail("AFFECTS", item.affects, Blue) 1981 ApprovalDetail("RISK", item.risk, Red) 1982 ApprovalDetail("ESTIMATE", item.estimate, Green) 1983 ApprovalDetail("ROLLBACK", item.rollback, TextMuted) 1984 ApprovalDetail("STATUS", item.status, Amber) 1985 1986 if (item.status == "Pending") { 1987 Text( 1988 text = "Approval applies only to this exact stage.", 1989 color = Green, 1990 fontSize = 12.sp 1991 ) 1992 1993 Spacer(modifier = Modifier.height(10.dp)) 1994 1995 FlowRow( 1996 horizontalArrangement = Arrangement.spacedBy(8.dp), 1997 verticalArrangement = Arrangement.spacedBy(8.dp) 1998 ) { 1999 Button( 2000 onClick = { 2001 onDecision(item.id, "Approved") 2002 } 2003 ) { 2004 Text("Approve Stage") 2005 } 2006 2007 OutlinedButton( 2008 onClick = { 2009 onDecision(item.id, "Changes Requested") 2010 } 2011 ) { 2012 Text("Request Changes") 2013 } 2014 2015 OutlinedButton( 2016 onClick = { 2017 onDecision(item.id, "Deferred") 2018 } 2019 ) { 2020 Text("Defer") 2021 } 2022 2023 Button( 2024 onClick = { 2025 onDecision(item.id, "Rejected") 2026 }, 2027 colors = ButtonDefaults.buttonColors( 2028 containerColor = Red, 2029 contentColor = Color.Black 2030 ) 2031 ) { 2032 Text("Reject") 2033 } 2034 } 2035 } 2036 } 2037 } 2038 }, 2039 confirmButton = { 2040 TextButton(onClick = onDismiss) { 2041 Text("Close") 2042 } 2043 }, 2044 containerColor = Surface 2045 ) 2046 } 2047 2048 @Composable 2049 private fun ApprovalDetail( 2050 label: String, 2051 value: String, 2052 valueColor: Color 2053 ) { 2054 SectionLabel(label) 2055 2056 Text( 2057 text = value, 2058 color = valueColor, 2059 lineHeight = 19.sp 2060 ) 2061 2062 Spacer(modifier = Modifier.height(8.dp)) 2063 } ============================================================ APPROVAL HUB STATUS ============================================================ * shire-approval-hub.service - SHiRE Private Approval Hub Loaded: loaded (/etc/systemd/system/shire-approval-hub.service; enabled; preset: enabled) Active: active (running) since Sun 2026-08-02 16:45:47 AWST; 1h 16min ago Main PID: 1098305 (python) Tasks: 2 (limit: 16648) Memory: 50.4M (peak: 52.5M) CPU: 1.742s CGroup: /system.slice/shire-approval-hub.service `-1098305 /home/shire3d/ARMOR/venv/bin/python /usr/local/lib/shire-approval-hub/server.py Aug 02 17:33:39 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:33:39] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:33:39 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:33:39] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:34:06 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:34:06] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:34:06 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:34:06] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:34:58 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:34:58] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:34:58 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:34:58] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:34:58 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:34:58] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:37:49 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:37:49] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:37:49 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:37:49] "GET /api/v1/approvals HTTP/1.1" 200 -100.110.126.100 - [02/Aug/2026 17:37:49] "GET /api/v1/approvals HTTP/1.1" 200 - Aug 02 17:52:50 Work python[1098305]: 100.110.126.100 - [02/Aug/2026 17:52:50] "GET /api/v1/approvals HTTP/1.1" 200 - # /etc/systemd/system/shire-approval-hub.service [Unit] Description=SHiRE Private Approval Hub After=network-online.target tailscaled.service Wants=network-online.target Requires=tailscaled.service [Service] Type=simple User=ray Group=ray Environment=PYTHONUNBUFFERED=1 EnvironmentFile=/etc/shire-approval-hub/approval-hub.env ExecStart=/home/shire3d/ARMOR/venv/bin/python /usr/local/lib/shire-approval-hub/server.py Restart=on-failure RestartSec=5 NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=read-only ReadWritePaths=/SHiREVault/SHiREApprovals /SHiREVault/SHiREMobile/Releases ReadOnlyPaths=/SHiREVault/SHiREAcademy/Practice [Install] WantedBy=multi-user.target