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