SHIRE MOBILE 0.7.5 — SENTINEL EXACT INTEGRATION ANCHORS ============================================================================ A. RECOVERED SENTINEL KOTLIN PAYLOAD ---------------------------------------------------------------------------- Path: /SHiREVault/Backup/OSBackups/SHIRE-MOBILE-SENTINEL-075-TRANSPLANT-BASELINE-20260805T120731Z/recovered-payloads/heredoc-001-line-00062-KOTLIN.txt Lines: 735 Bytes: 18890 Package and imports: package com.shire.mobile import android.Manifest import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.app.job.JobInfo import android.app.job.JobParameters import android.app.job.JobScheduler import android.app.job.JobService import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import android.util.Log import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import java.util.LinkedHashSet import java.util.concurrent.atomic.AtomicBoolean Top-level declarations: - class SentinelMobileAlert( - object SentinelMobileAlerts - fun initialise(context: Context) - fun ensureChannels(context: Context) - fun startLink(context: Context) - fun scheduleFallback(context: Context) - fun linkNotification( - fun poll(context: Context): Int - fun fetchAlerts(): List - fun approvedTailnetHost( - fun isImportant( - fun notifyAlert( - fun trimSeen( - fun pollDelay(): Long = - fun linkNotificationId(): Int = - class SentinelAlertLinkService : Service() - fun onCreate() - fun onStartCommand( - fun onDestroy() - fun onBind( - class SentinelAlertPollJobService : JobService() - fun onStartJob( - fun onStopJob( - class SentinelAlertBootReceiver : BroadcastReceiver() - fun onReceive( Constants and fixed configuration: - TAG = "SHiRESentinel" - ALERT_CHANNEL = "shire_sentinel_mobile_alerts" - LINK_CHANNEL = "shire_sentinel_mobile_link" - PREFS = "shire_sentinel_mobile_alert_state" - KEY_INITIALISED = "initialised" - KEY_SEEN = "seen_alert_ids" - LINK_NOTIFICATION_ID = 73010 - FALLBACK_JOB_ID = 73011 - MAX_SEEN_IDS = 500 - POLL_DELAY_MS = 30_000L Relevant string literals: - SHiRESentinel - shire_sentinel_mobile_alerts - shire_sentinel_mobile_link - shire_sentinel_mobile_alert_state - seen_alert_ids - SENTiNEL security alerts - Private security and family-safety alerts from SHiRE Mini - SENTiNEL private link - Keeps the private Tailscale SENTiNEL alert link running - SENTiNEL private link running - Watching SHiRE Mini through Tailscale - POLL_SEEDED alerts=${alerts.size} - POLL_READY alerts=${alerts.size} delivered=$delivered - SENTiNEL gateway configuration is incomplete - $base/v1/sentinel/status - SENTiNEL refused a non-Tailscale endpoint - SENTiNEL gateway returned HTTP $status - recent_alerts - SENTiNEL alert - category - ${alert.title} ${alert.category} - NOTIFICATION_PERMISSION_MISSING - sentinel_alert_id - \nCategory: - SENTiNEL mobile self-test - SENTiNEL $severityLabel alert - shire_sentinel_alerts - MOBILE_SELF_TEST id=${alert.id} - MOBILE_ALERT id=${alert.id} - severity=${alert.severity} - POLL_FAILED - shire-sentinel-mobile-link - FALLBACK_POLL_FAILED - shire-sentinel-fallback-poll Component/lifecycle/polling context: 00001: package com.shire.mobile 00002: 00003: import android.Manifest 00004: import android.app.Notification 00005: import android.app.NotificationChannel 00006: import android.app.NotificationManager 00007: import android.app.PendingIntent 00008: import android.app.Service 00009: import android.app.job.JobInfo 00010: import android.app.job.JobParameters 00011: import android.app.job.JobScheduler 00012: import android.app.job.JobService 00013: import android.content.BroadcastReceiver 00014: import android.content.ComponentName 00015: import android.content.Context 00016: import android.content.Intent ... 00053: "seen_alert_ids" 00054: 00055: private const val LINK_NOTIFICATION_ID = 73010 00056: private const val FALLBACK_JOB_ID = 73011 00057: private const val MAX_SEEN_IDS = 500 00058: private const val POLL_DELAY_MS = 30_000L 00059: 00060: private val importantKeywords = listOf( 00061: "adult", 00062: "porn", 00063: "explicit", ... 00083: "bypass", 00084: ) 00085: 00086: fun initialise(context: Context) { 00087: ensureChannels(context) 00088: scheduleFallback(context) 00089: startLink(context) 00090: } 00091: 00092: fun ensureChannels(context: Context) { 00093: val manager = 00094: context.getSystemService( 00095: NotificationManager::class.java, 00096: ) 00097: 00098: val alertChannel = NotificationChannel( 00099: ALERT_CHANNEL, 00100: "SENTiNEL security alerts", 00101: NotificationManager.IMPORTANCE_HIGH, 00102: ).apply { 00103: description = ... 00105: enableVibration(true) 00106: lockscreenVisibility = 00107: Notification.VISIBILITY_PRIVATE 00108: } 00109: 00110: val linkChannel = NotificationChannel( 00111: LINK_CHANNEL, 00112: "SENTiNEL private link", 00113: NotificationManager.IMPORTANCE_LOW, 00114: ).apply { 00115: description = ... 00117: setShowBadge(false) 00118: lockscreenVisibility = 00119: Notification.VISIBILITY_PRIVATE 00120: } 00121: 00122: manager.createNotificationChannel(alertChannel) 00123: manager.createNotificationChannel(linkChannel) 00124: } 00125: 00126: fun startLink(context: Context) { 00127: val intent = Intent( 00128: context, 00129: SentinelAlertLinkService::class.java, 00130: ) 00131: 00132: try { 00133: context.startForegroundService(intent) 00134: } catch (error: Exception) { 00135: Log.w( 00136: TAG, 00137: "LINK_START_FAILED " + 00138: "${error.javaClass.simpleName}: ${error.message}", 00139: ) 00140: } 00141: } 00142: 00143: fun scheduleFallback(context: Context) { 00144: val scheduler = 00145: context.getSystemService( 00146: JobScheduler::class.java, 00147: ) 00148: 00149: val component = ComponentName( 00150: context, 00151: SentinelAlertPollJobService::class.java, ... 00160: ) 00161: .setPersisted(true) 00162: .setPeriodic(15L * 60L * 1000L) 00163: .build() 00164: 00165: val result = scheduler.schedule(job) 00166: 00167: Log.i( 00168: TAG, 00169: "FALLBACK_SCHEDULED result=$result", 00170: ) 00171: } 00172: 00173: internal fun linkNotification( 00174: context: Context, ... 00205: 00206: internal fun poll(context: Context): Int { 00207: val alerts = fetchAlerts() 00208: 00209: val preferences = 00210: context.getSharedPreferences( 00211: PREFS, 00212: Context.MODE_PRIVATE, 00213: ) 00214: 00215: val stored = ... 00238: .putBoolean(KEY_INITIALISED, true) 00239: .apply() 00240: 00241: Log.i( 00242: TAG, 00243: "POLL_SEEDED alerts=${alerts.size}", 00244: ) 00245: 00246: return 0 00247: } 00248: ... 00267: .putStringSet(KEY_SEEN, HashSet(seen)) 00268: .apply() 00269: 00270: Log.i( 00271: TAG, 00272: "POLL_READY alerts=${alerts.size} delivered=$delivered", 00273: ) 00274: 00275: return delivered 00276: } 00277: ... 00460: ): Boolean { 00461: if ( 00462: Build.VERSION.SDK_INT >= 00463: Build.VERSION_CODES.TIRAMISU && 00464: context.checkSelfPermission( 00465: Manifest.permission.POST_NOTIFICATIONS, 00466: ) != PackageManager.PERMISSION_GRANTED 00467: ) { 00468: Log.w( 00469: TAG, 00470: "NOTIFICATION_PERMISSION_MISSING", ... 00571: ) 00572: 00573: if (alert.selfTest) { 00574: Log.i( 00575: TAG, 00576: "MOBILE_SELF_TEST id=${alert.id}", 00577: ) 00578: } else { 00579: Log.i( 00580: TAG, 00581: "MOBILE_ALERT id=${alert.id} " + ... 00603: 00604: internal fun linkNotificationId(): Int = 00605: LINK_NOTIFICATION_ID 00606: } 00607: 00608: class SentinelAlertLinkService : Service() { 00609: private val running = 00610: AtomicBoolean(false) 00611: 00612: private var worker: Thread? = 00613: null 00614: 00615: override fun onCreate() { 00616: super.onCreate() 00617: 00618: SentinelMobileAlerts.ensureChannels(this) 00619: 00620: val notification = 00621: SentinelMobileAlerts.linkNotification(this) 00622: 00623: if (Build.VERSION.SDK_INT >= 34) { 00624: startForeground( 00625: SentinelMobileAlerts.linkNotificationId(), 00626: notification, 00627: ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, 00628: ) 00629: } else { 00630: startForeground( 00631: SentinelMobileAlerts.linkNotificationId(), 00632: notification, 00633: ) 00634: } 00635: ... 00663: isDaemon = true 00664: start() 00665: } 00666: } 00667: 00668: override fun onStartCommand( 00669: intent: Intent?, 00670: flags: Int, 00671: startId: Int, 00672: ): Int = 00673: START_STICKY 00674: 00675: override fun onDestroy() { 00676: running.set(false) 00677: worker?.interrupt() 00678: worker = null 00679: super.onDestroy() 00680: } ... 00683: intent: Intent?, 00684: ): IBinder? = 00685: null 00686: } 00687: 00688: class SentinelAlertPollJobService : JobService() { 00689: override fun onStartJob( 00690: parameters: JobParameters, 00691: ): Boolean { 00692: Thread( 00693: { 00694: try { ... 00711: ).start() 00712: 00713: return true 00714: } 00715: 00716: override fun onStopJob( 00717: parameters: JobParameters, 00718: ): Boolean = 00719: true 00720: } 00721: 00722: class SentinelAlertBootReceiver : BroadcastReceiver() { 00723: override fun onReceive( 00724: context: Context, 00725: intent: Intent, 00726: ) { 00727: SentinelMobileAlerts.ensureChannels(context) 00728: SentinelMobileAlerts.scheduleFallback(context) 00729: 00730: Log.i( 00731: "SHiRESentinel", 00732: "BOOT_FALLBACK_READY action=${intent.action}", 00733: ) B. OLD INSTALLER PATCH LOGIC ---------------------------------------------------------------------------- Patch payload 1: /SHiREVault/Backup/OSBackups/SHIRE-MOBILE-SENTINEL-075-TRANSPLANT-BASELINE-20260805T120731Z/recovered-payloads/heredoc-002-line-00799-PY.txt Patch payload 2: /SHiREVault/Backup/OSBackups/SHIRE-MOBILE-SENTINEL-075-TRANSPLANT-BASELINE-20260805T120731Z/recovered-payloads/heredoc-003-line-00974-PY.txt MainActivity/manifest/version patch context: 00023: addition = """ ApprovalNotifications.checkNow(this) 00024: 00025: // SHIRE-MOBILE-SENTINEL-LIVE-ALERTS-0730 00026: if ( 00027: android.os.Build.VERSION.SDK_INT >= 33 && 00028: checkSelfPermission( 00029: android.Manifest.permission.POST_NOTIFICATIONS 00030: ) != android.content.pm.PackageManager.PERMISSION_GRANTED 00031: ) { 00032: requestPermissions( 00033: arrayOf( 00034: android.Manifest.permission.POST_NOTIFICATIONS 00035: ), 00036: 7301 00037: ) 00038: } 00039: 00040: SentinelMobileAlerts.initialise(this)""" 00041: 00042: if marker not in main_text: 00043: if anchor not in main_text: 00044: raise SystemExit( 00045: "MainActivity notification anchor was not found." 00046: ) 00047: 00048: main_text = main_text.replace( 00049: anchor, 00050: addition, 00051: 1, ... 00063: permission_anchor = ( 00064: ' ' 00066: ) 00067: 00068: permission_block = """ 00069: 00070: """ 00071: 00072: if ( 00073: "android.permission.FOREGROUND_SERVICE_SPECIAL_USE" 00074: not in manifest 00075: ): 00076: if permission_anchor not in manifest: 00077: raise SystemExit( 00078: "Manifest permission anchor was not found." 00079: ) ... 00087: component_marker = ( 00088: "SHIRE-MOBILE-SENTINEL-LIVE-ALERTS-0730" 00089: ) 00090: 00091: components = """ 00092: 00093: 00098: 00099: 00102: 00103: 00104: 00105: 00109: 00110: 00114: 00115: 00116: 00117: ... 00139: 00140: build_text = build_path.read_text( 00141: encoding="utf-8", 00142: ) 00143: 00144: match = re.search( 00145: r"versionCode\s*=\s*(\d+)", 00146: build_text, 00147: ) 00148: 00149: if not match: 00150: raise SystemExit( 00151: "Android versionCode was not found." 00152: ) 00153: 00154: new_code = int(match.group(1)) + 1 00155: 00156: build_text = re.sub( 00157: r"versionCode\s*=\s*\d+", 00158: f"versionCode = {new_code}", 00159: build_text, 00160: count=1, 00161: ) 00162: 00163: build_text = re.sub( 00164: r'versionName\s*=\s*"[^"]+"', 00165: 'versionName = "0.7.3-sentinel-mobile-alerts"', 00166: build_text, 00167: count=1, 00168: ) 00169: 00170: build_path.write_text( 00171: build_text, ... 00184: encoding="utf-8", 00185: ).splitlines() 00186: 00187: output = [ 00188: line 00189: for line in lines 00190: if not line.startswith( 00191: "SHIRE_MOBILE_GATEWAY_PREVIOUS_TOKEN=" 00192: ) 00193: ] 00194: 00195: target.write_text( 00196: "\n".join(output) + "\n", C. CURRENT 0.7.5 MAINACTIVITY ---------------------------------------------------------------------------- Path: /home/ray/ARMOR/apps/shire-mobile/app/src/main/java/com/shire/mobile/MainActivity.kt Lines: 2159 Bytes: 67797 Current imports: import android.content.Intent import android.os.Bundle import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay Current declarations: - class Screen - class ShireExpression - class ShireMode - class Skill( - class ApprovalItem( - class MainActivity : ComponentActivity() - fun onCreate(savedInstanceState: Bundle?) - fun ShireMobileApp() - fun recommendedApproval( - fun FoldedCompanion( - fun FoldCommandCentre( - fun ModeSelector( - fun ModeWorkspace(mode: ShireMode) - fun AppHeader(compact: Boolean) - fun RealShireFace( - fun CompactStatus(pendingApprovals: Int) - fun StatusChip( - fun MainActionPanel( - fun CurrentMissionCard() - fun CurrentLearningCard() - fun PersonalityAnimationControl( - fun CommandCentreOverview( - fun OverviewMetricRow(approvals: Int) - fun MetricCard(value: String, label: String) - fun AcademySummaryCard(onOpen: () -> Unit) - fun ApprovalSummaryCard( - fun MiniAcademy( - fun AcademyHero() - fun SkillList( - fun SkillDetail(skill: Skill) - fun LearningQueueCard() - fun QueueLine(name: String, state: String) - fun RecommendationCard(onAdd: () -> Unit) - fun SystemGuardrailsCard() - fun GuardrailLine(name: String, state: String) - fun DetailLine(label: String, value: String) - fun SectionLabel(text: String) - fun LearnAndPrintDialog( - fun ApprovalCentreDialog( - fun ApprovalDetail( Lifecycle and integration-anchor context: 00001: package com.shire.mobile 00002: 00003: import android.content.Intent 00004: import android.os.Bundle 00005: import android.provider.Settings 00006: import androidx.activity.ComponentActivity 00007: import androidx.activity.compose.setContent 00008: import androidx.compose.animation.AnimatedContent 00009: import androidx.compose.animation.AnimatedVisibility 00010: import androidx.compose.animation.core.Animatable 00011: import androidx.compose.animation.core.RepeatMode 00012: import androidx.compose.animation.core.animateFloat 00013: import androidx.compose.animation.core.infiniteRepeatable 00014: import androidx.compose.animation.core.rememberInfiniteTransition ... 00092: Academy 00093: } 00094: 00095: private enum class ShireExpression { 00096: Normal, 00097: Happy, 00098: Thinking, 00099: Sentinel, 00100: Concerned 00101: } 00102: 00103: private enum class ShireMode { 00104: Calm, 00105: Academy, 00106: Sentinel, 00107: Forge 00108: } 00109: 00110: private data class Skill( 00111: val id: String, 00112: val name: String, 00113: val category: String, ... 00115: val progress: Float, 00116: val state: String, 00117: val evidence: Int, 00118: val result: String, 00119: val forge: String 00120: ) 00121: 00122: private data class ApprovalItem( 00123: val id: Int, 00124: val title: String, 00125: val stage: String, 00126: val purpose: String, 00127: val affects: String, 00128: val risk: String, 00129: val estimate: String, 00130: val rollback: String, 00131: val status: String = "Pending" 00132: ) 00133: 00134: class MainActivity : ComponentActivity() { 00135: override fun onCreate(savedInstanceState: Bundle?) { 00136: super.onCreate(savedInstanceState) 00137: 00138: // SHIRE-MOBILE-APPROVAL-INBOX-0001 00139: ApprovalNotifications.ensureChannel(this) 00140: ApprovalPollJobService.schedule(this) 00141: ApprovalNotifications.checkNow(this) 00142: 00143: setContent { 00144: MaterialTheme( 00145: colorScheme = darkColorScheme( 00146: primary = Purple, 00147: secondary = Blue, 00148: background = Background, 00149: surface = Surface, 00150: onPrimary = Color.Black, ... 00174: mutableStateOf(systemAnimationsEnabled) 00175: } 00176: 00177: var screen by remember { mutableStateOf(Screen.Home) } 00178: var mode by remember { mutableStateOf(ShireMode.Calm) } 00179: var expression by remember { mutableStateOf(ShireExpression.Normal) } 00180: var showLearnPrint by remember { mutableStateOf(false) } 00181: var showApprovals by remember { mutableStateOf(false) } 00182: var message by remember { 00183: mutableStateOf("Systems ready, Chief. What are we building today?") 00184: } 00185: 00186: val handleModeChange: (ShireMode) -> Unit = { selected -> 00187: mode = selected 00188: ... 00195: 00196: ShireMode.Academy -> { 00197: screen = Screen.Academy 00198: expression = ShireExpression.Thinking 00199: message = "Academy mode ready. Let us review what I have learned." 00200: } 00201: 00202: ShireMode.Sentinel -> { 00203: screen = Screen.Home 00204: expression = ShireExpression.Sentinel 00205: message = "Sentinel mode active. Security systems have priority." 00206: } 00207: 00208: ShireMode.Forge -> { 00209: screen = Screen.Home 00210: expression = ShireExpression.Happy 00211: message = "Forge mode ready. Let us build something remarkable." 00212: } 00213: } 00214: } 00215: 00216: var approvals by remember { 00217: mutableStateOf(emptyList()) 00218: } 00219: 00220: BoxWithConstraints( 00221: modifier = Modifier 00222: .fillMaxSize() 00223: .background(Background) 00224: ) { ... 00228: if (expanded) { 00229: FoldCommandCentre( 00230: modifier = Modifier.padding(paddingValues), 00231: screen = screen, 00232: mode = mode, 00233: expression = expression, 00234: message = message, 00235: approvals = approvals, 00236: personalityAnimations = personalityAnimations, 00237: onPersonalityAnimationsChange = { 00238: personalityAnimations = it 00239: }, 00240: onScreenChange = { screen = it }, 00241: onModeChange = handleModeChange, 00242: onExpressionChange = { expression = it }, 00243: onMessageChange = { message = it }, 00244: onLearnPrint = { 00245: expression = ShireExpression.Thinking 00246: showLearnPrint = true 00247: }, 00248: onApprovals = { 00249: expression = ShireExpression.Thinking 00250: context.startActivity( 00251: Intent( 00252: context, 00253: ApprovalInboxActivity::class.java 00254: ) 00255: ) 00256: }, 00257: onAddRecommendation = { 00258: approvals = approvals + recommendedApproval(approvals) 00259: expression = ShireExpression.Happy 00260: message = "Learning recommendation added for approval." 00261: } 00262: ) 00263: } else { 00264: FoldedCompanion( 00265: modifier = Modifier.padding(paddingValues), 00266: screen = screen, 00267: mode = mode, 00268: expression = expression, 00269: message = message, 00270: approvals = approvals, 00271: personalityAnimations = personalityAnimations, 00272: onScreenChange = { screen = it }, 00273: onModeChange = handleModeChange, 00274: onExpressionChange = { expression = it }, 00275: onMessageChange = { message = it }, 00276: onLearnPrint = { 00277: expression = ShireExpression.Thinking 00278: showLearnPrint = true 00279: }, 00280: onApprovals = { 00281: expression = ShireExpression.Thinking 00282: context.startActivity( 00283: Intent( 00284: context, 00285: ApprovalInboxActivity::class.java 00286: ) 00287: ) 00288: }, 00289: onAddRecommendation = { 00290: approvals = approvals + recommendedApproval(approvals) 00291: expression = ShireExpression.Happy 00292: message = "Learning recommendation added for approval." 00293: } 00294: ) 00295: } 00296: } 00297: } 00298: 00299: if (showLearnPrint) { 00300: LearnAndPrintDialog( 00301: onDismiss = { 00302: showLearnPrint = false 00303: expression = ShireExpression.Normal 00304: }, 00305: onCreatePlan = { request, mode -> 00306: approvals = approvals + ApprovalItem( 00307: id = (approvals.maxOfOrNull { it.id } ?: 0) + 1, 00308: title = request.take(55), 00309: stage = "Approve Learning", 00310: purpose = request, 00311: affects = "Creates a Safe Plan only. No learning, design, slicing or printing starts.", 00312: risk = "Awaiting safety, licensing and feasibility analysis.", 00313: estimate = mode, 00314: rollback = "Fully cancellable before staged approval." 00315: ) 00316: 00317: showLearnPrint = false 00318: expression = ShireExpression.Happy 00319: message = "Safe Plan added to the Approval Centre." 00320: } 00321: ) 00322: } 00323: 00324: if (showApprovals) { 00325: ApprovalCentreDialog( 00326: approvals = approvals, 00327: onDismiss = { 00328: showApprovals = false 00329: expression = ShireExpression.Normal 00330: }, 00331: onDecision = { id, decision -> 00332: approvals = approvals.map { 00333: if (it.id == id) it.copy(status = decision) else it 00334: } 00335: 00336: expression = when (decision) { 00337: "Approved" -> ShireExpression.Happy 00338: "Rejected" -> ShireExpression.Concerned 00339: "Changes Requested" -> ShireExpression.Thinking ... 00341: } 00342: 00343: message = when (decision) { 00344: "Approved" -> "Only that exact stage was approved." 00345: "Rejected" -> "Plan rejected. No action will be taken." 00346: "Deferred" -> "Plan deferred and remains inactive." 00347: "Changes Requested" -> "A revised plan is now required." 00348: else -> "Approval updated." 00349: } 00350: } 00351: ) 00352: } 00353: } 00354: 00355: private fun recommendedApproval( 00356: approvals: List 00357: ): ApprovalItem { 00358: return ApprovalItem( 00359: id = (approvals.maxOfOrNull { it.id } ?: 0) + 1, 00360: title = "Parametric phone support geometry", 00361: stage = "Approve Learning", 00362: purpose = "Learn reusable phone-fit and support geometry for DockForge and Forge.", 00363: affects = "Adds an Academy Safe Plan only.", 00364: risk = "Low. No certification or Forge-permission changes.", 00365: estimate = "45–90 minutes.", 00366: rollback = "Can be rejected, deferred or cancelled." ... 00370: @Composable 00371: private fun FoldedCompanion( 00372: modifier: Modifier, 00373: screen: Screen, 00374: mode: ShireMode, 00375: expression: ShireExpression, 00376: message: String, 00377: approvals: List, 00378: personalityAnimations: Boolean, 00379: onScreenChange: (Screen) -> Unit, 00380: onModeChange: (ShireMode) -> Unit, 00381: onExpressionChange: (ShireExpression) -> Unit, 00382: onMessageChange: (String) -> Unit, 00383: onLearnPrint: () -> Unit, 00384: onApprovals: () -> Unit, 00385: onAddRecommendation: () -> Unit 00386: ) { 00387: Column( 00388: modifier = modifier 00389: .fillMaxSize() 00390: .padding(horizontal = 17.dp, vertical = 13.dp), 00391: horizontalAlignment = Alignment.CenterHorizontally ... 00429: fontSize = 16.sp, 00430: lineHeight = 22.sp 00431: ) 00432: 00433: Spacer(modifier = Modifier.height(14.dp)) 00434: 00435: CompactStatus( 00436: pendingApprovals = approvals.count { 00437: it.status == "Pending" 00438: } 00439: ) 00440: 00441: Spacer(modifier = Modifier.height(12.dp)) 00442: 00443: MainActionPanel( 00444: pendingApprovals = approvals.count { 00445: it.status == "Pending" 00446: }, 00447: onLearnPrint = onLearnPrint, 00448: onApprovals = onApprovals, 00449: onAcademy = { 00450: onScreenChange(Screen.Academy) 00451: }, 00452: onShire = { 00453: onExpressionChange( 00454: ShireExpression.Normal 00455: ) ... 00485: 00486: Screen.Academy -> { 00487: MiniAcademy( 00488: expanded = false, 00489: onBack = { 00490: onScreenChange(Screen.Home) 00491: }, 00492: onApprovals = onApprovals, 00493: onAddRecommendation = onAddRecommendation 00494: ) 00495: } 00496: } 00497: } 00498: } 00499: } ... 00501: @Composable 00502: private fun FoldCommandCentre( 00503: modifier: Modifier, 00504: screen: Screen, 00505: mode: ShireMode, 00506: expression: ShireExpression, 00507: message: String, 00508: approvals: List, 00509: personalityAnimations: Boolean, 00510: onPersonalityAnimationsChange: (Boolean) -> Unit, 00511: onScreenChange: (Screen) -> Unit, 00512: onModeChange: (ShireMode) -> Unit, 00513: onExpressionChange: (ShireExpression) -> Unit, 00514: onMessageChange: (String) -> Unit, 00515: onLearnPrint: () -> Unit, 00516: onApprovals: () -> Unit, 00517: onAddRecommendation: () -> Unit 00518: ) { 00519: Row( 00520: modifier = modifier 00521: .fillMaxSize() 00522: .padding(18.dp), 00523: horizontalArrangement = Arrangement.spacedBy(16.dp) ... 00559: Spacer(modifier = Modifier.height(13.dp)) 00560: 00561: CurrentMissionCard() 00562: 00563: Spacer(modifier = Modifier.height(12.dp)) 00564: 00565: MainActionPanel( 00566: pendingApprovals = approvals.count { it.status == "Pending" }, 00567: onLearnPrint = onLearnPrint, 00568: onApprovals = onApprovals, 00569: onAcademy = { onScreenChange(Screen.Academy) }, 00570: onShire = { 00571: onExpressionChange(ShireExpression.Normal) 00572: onMessageChange("Local SHiRE mode selected.") 00573: }, 00574: onChatGpt = { 00575: onExpressionChange(ShireExpression.Thinking) ... 00596: colors = CardDefaults.cardColors(containerColor = Surface), 00597: shape = RoundedCornerShape(24.dp) 00598: ) { 00599: when (screen) { 00600: Screen.Home -> { 00601: CommandCentreOverview( 00602: mode = mode, 00603: approvals = approvals, 00604: onAcademy = { onScreenChange(Screen.Academy) }, 00605: onApprovals = onApprovals, 00606: onAddRecommendation = onAddRecommendation 00607: ) 00608: } 00609: 00610: Screen.Academy -> { 00611: MiniAcademy( 00612: expanded = true, 00613: onBack = { onScreenChange(Screen.Home) }, 00614: onApprovals = onApprovals, 00615: onAddRecommendation = onAddRecommendation 00616: ) 00617: } 00618: } 00619: } 00620: } 00621: } ... 00643: selected = selected == mode, 00644: onClick = { onSelect(mode) }, 00645: label = { 00646: Text( 00647: when (mode) { 00648: ShireMode.Calm -> "Calm" 00649: ShireMode.Academy -> "Academy" 00650: ShireMode.Sentinel -> "Sentinel" 00651: ShireMode.Forge -> "Forge" 00652: } 00653: ) 00654: } 00655: ) 00656: } 00657: } ... 00687: details = listOf( 00688: "Current skill" to "Boolean Operations", 00689: "Progress" to "72%", 00690: "Protected skills" to "371" 00691: ) 00692: } 00693: 00694: ShireMode.Sentinel -> { 00695: title = "Sentinel" 00696: subtitle = "Security posture, network alerts and protected actions." 00697: status = "Protected" 00698: accent = Red 00699: details = listOf( 00700: "Threat posture" to "Monitoring", 00701: "Network control" to "Approval required", 00702: "Public ports" to "None" 00703: ) 00704: } 00705: 00706: ShireMode.Forge -> { 00707: title = "Forge" 00708: subtitle = "Design, modelling, slicing and printer readiness." 00709: status = "Ready" 00710: accent = Purple 00711: details = listOf( 00712: "Design engine" to "CadQuery available", 00713: "Print actions" to "Approval locked", 00714: "Default material" to "PLA" 00715: ) 00716: } 00717: } 00718: 00719: Card( 00720: modifier = Modifier.fillMaxWidth(), ... 00852: } 00853: } 00854: 00855: val drawable = when (expression) { 00856: ShireExpression.Normal -> R.drawable.shire_face_main 00857: ShireExpression.Happy -> R.drawable.shire_face_happy 00858: ShireExpression.Thinking -> R.drawable.shire_face_thinking 00859: ShireExpression.Sentinel -> R.drawable.shire_face_sentinel 00860: ShireExpression.Concerned -> R.drawable.shire_face_scared 00861: } 00862: 00863: Column(horizontalAlignment = Alignment.CenterHorizontally) { 00864: Card( 00865: modifier = Modifier 00866: .size(if (expanded) 330.dp else 238.dp) ... 00897: fontWeight = FontWeight.SemiBold 00898: ) 00899: } 00900: } 00901: } 00902: 00903: @Composable 00904: private fun CompactStatus(pendingApprovals: Int) { 00905: FlowRow( 00906: modifier = Modifier.fillMaxWidth(), 00907: horizontalArrangement = Arrangement.spacedBy(7.dp), 00908: verticalArrangement = Arrangement.spacedBy(7.dp) 00909: ) { 00910: StatusChip("ACADEMY", "Running", Green) 00911: StatusChip("APPROVALS", "Open inbox", Amber) 00912: StatusChip("SENTINEL", "Protected", Green) 00913: } 00914: } 00915: 00916: @Composable 00917: private fun StatusChip( 00918: title: String, 00919: value: String, ... 00944: ) 00945: } 00946: } 00947: } 00948: 00949: @Composable 00950: private fun MainActionPanel( 00951: pendingApprovals: Int, 00952: onLearnPrint: () -> Unit, 00953: onApprovals: () -> Unit, 00954: onAcademy: () -> Unit, 00955: onShire: () -> Unit, 00956: onChatGpt: () -> Unit, 00957: onBoth: () -> Unit 00958: ) { 00959: Card( 00960: modifier = Modifier ... 00993: contentColor = Color.Black 00994: ) 00995: ) { 00996: Text("ACADEMY", fontWeight = FontWeight.Bold) 00997: } 00998: 00999: Button( 01000: onClick = onApprovals, 01001: modifier = Modifier.weight(1f), 01002: colors = ButtonDefaults.buttonColors( 01003: containerColor = Amber, 01004: contentColor = Color.Black 01005: ) 01006: ) { 01007: Text("APPROVALS") 01008: } 01009: } 01010: 01011: Row(horizontalArrangement = Arrangement.spacedBy(7.dp)) { 01012: val context = LocalContext.current 01013: 01014: OutlinedButton( 01015: onClick = { 01016: context.startActivity( 01017: Intent(context, SentinelActivity::class.java) 01018: ) 01019: }, 01020: modifier = Modifier.weight(1f) 01021: ) { 01022: Text("SENTINEL") 01023: } 01024: 01025: OutlinedButton( 01026: onClick = { 01027: context.startActivity( 01028: Intent(context, NfcGuardActivity::class.java) 01029: ) 01030: }, 01031: modifier = Modifier.weight(1f) 01032: ) { 01033: Text("NFC") 01034: } 01035: 01036: OutlinedButton( 01037: onClick = { 01038: context.startActivity( 01039: Intent(context, VoiceLabActivity::class.java) 01040: ) 01041: }, 01042: modifier = Modifier.weight(1f) 01043: ) { 01044: Text("VOICE") 01045: } 01046: } ... 01049: horizontalArrangement = Arrangement.spacedBy(8.dp) 01050: ) { 01051: val context = LocalContext.current 01052: 01053: Button( 01054: onClick = { 01055: context.startActivity( 01056: Intent( 01057: context, 01058: LimbForgeMeasurementActivity::class.java 01059: ) 01060: ) 01061: }, 01062: modifier = Modifier.weight(1f), 01063: colors = ButtonDefaults.buttonColors( 01064: containerColor = Green, 01065: contentColor = Color.Black 01066: ) 01067: ) { 01068: Text( 01069: "LIMBFORGE", 01070: fontWeight = FontWeight.Black 01071: ) 01072: } 01073: 01074: Button( 01075: onClick = { 01076: context.startActivity( 01077: Intent( 01078: context, 01079: AgentPortalActivity::class.java 01080: ).apply { 01081: putExtra( 01082: AgentPortalActivity.EXTRA_AGENT_TITLE, 01083: "CoverCanvas" 01084: ) ... 01233: } 01234: } 01235: } 01236: 01237: @Composable 01238: private fun CommandCentreOverview( 01239: mode: ShireMode, 01240: approvals: List, 01241: onAcademy: () -> Unit, 01242: onApprovals: () -> Unit, 01243: onAddRecommendation: () -> Unit 01244: ) { 01245: Column( 01246: modifier = Modifier 01247: .fillMaxSize() 01248: .verticalScroll(rememberScrollState()) 01249: .padding(18.dp) ... 01262: Spacer(modifier = Modifier.height(15.dp)) 01263: 01264: ModeWorkspace(mode) 01265: 01266: Spacer(modifier = Modifier.height(14.dp)) 01267: 01268: OverviewMetricRow( 01269: approvals = approvals.count { it.status == "Pending" } 01270: ) 01271: 01272: Spacer(modifier = Modifier.height(14.dp)) 01273: 01274: AcademySummaryCard(onOpen = onAcademy) 01275: 01276: Spacer(modifier = Modifier.height(12.dp)) 01277: 01278: ApprovalSummaryCard( 01279: approvals = approvals, 01280: onOpen = onApprovals 01281: ) 01282: 01283: Spacer(modifier = Modifier.height(12.dp)) 01284: 01285: RecommendationCard(onAdd = onAddRecommendation) 01286: 01287: Spacer(modifier = Modifier.height(12.dp)) 01288: 01289: SystemGuardrailsCard() 01290: } 01291: } 01292: 01293: @Composable 01294: private fun OverviewMetricRow(approvals: Int) { 01295: FlowRow( 01296: horizontalArrangement = Arrangement.spacedBy(9.dp), 01297: verticalArrangement = Arrangement.spacedBy(9.dp) 01298: ) { 01299: MetricCard("371", "Skills") 01300: MetricCard("72%", "Current skill") 01301: MetricCard("$approvals", "Approvals") 01302: MetricCard("SAFE", "Guardrails") 01303: } 01304: } 01305: 01306: @Composable 01307: private fun MetricCard(value: String, label: String) { 01308: Card( ... 01369: fontSize = 12.sp 01370: ) 01371: } 01372: } 01373: } 01374: 01375: @Composable 01376: private fun ApprovalSummaryCard( 01377: approvals: List, 01378: onOpen: () -> Unit 01379: ) { 01380: Card( 01381: modifier = Modifier 01382: .fillMaxWidth() 01383: .clickable(onClick = onOpen), 01384: colors = CardDefaults.cardColors(containerColor = Raised), 01385: shape = RoundedCornerShape(18.dp) 01386: ) { 01387: Column(modifier = Modifier.padding(16.dp)) { 01388: SectionLabel("APPROVAL CENTRE") 01389: 01390: Text( 01391: text = "${approvals.count { it.status == "Pending" }} actions waiting", 01392: fontSize = 18.sp, 01393: fontWeight = FontWeight.Bold, 01394: color = Amber 01395: ) 01396: 01397: approvals 01398: .filter { it.status == "Pending" } 01399: .take(2) 01400: .forEach { 01401: Text( 01402: text = "• ${it.stage}: ${it.title}", 01403: color = TextMuted, 01404: fontSize = 13.sp ... 01408: } 01409: } 01410: 01411: @Composable 01412: private fun MiniAcademy( 01413: expanded: Boolean, 01414: onBack: () -> Unit, 01415: onApprovals: () -> Unit, 01416: onAddRecommendation: () -> Unit 01417: ) { 01418: val skills = remember { 01419: listOf( 01420: Skill( 01421: id = "boolean", 01422: name = "Boolean Operations", ... 01479: Text( 01480: text = "Running • protected read-only view", 01481: color = Green, 01482: fontSize = 12.sp 01483: ) 01484: } 01485: 01486: OutlinedButton(onClick = onApprovals) { 01487: Text("Approvals") 01488: } 01489: } 01490: 01491: Spacer(modifier = Modifier.height(14.dp)) 01492: 01493: AcademyHero() 01494: ... 01700: DetailLine("Evidence records", skill.evidence.toString()) 01701: DetailLine("Current result", skill.result) 01702: DetailLine("Forge permission", skill.forge) 01703: 01704: Spacer(modifier = Modifier.height(10.dp)) 01705: 01706: Text( 01707: text = "Mobile Academy is read-only. Any change requires a Safe Plan and explicit approval.", 01708: color = Green, 01709: fontSize = 12.sp, 01710: lineHeight = 17.sp 01711: ) 01712: } 01713: } 01714: } ... 01782: text = "Benefit: Forge + income", 01783: color = Green 01784: ) 01785: 01786: Spacer(modifier = Modifier.height(10.dp)) 01787: 01788: Button(onClick = onAdd) { 01789: Text("Add for Approval") 01790: } 01791: } 01792: } 01793: } 01794: 01795: @Composable 01796: private fun SystemGuardrailsCard() { ... 01800: shape = RoundedCornerShape(19.dp) 01801: ) { 01802: Column(modifier = Modifier.padding(16.dp)) { 01803: SectionLabel("GUARDRAILS") 01804: 01805: GuardrailLine("Existing 371-skill state", "Protected") 01806: GuardrailLine("Counted attempts", "Unchanged") 01807: GuardrailLine("Certification", "Approval required") 01808: GuardrailLine("Forge permissions", "Locked by default") 01809: GuardrailLine("Automatic printing", "Disabled") 01810: } 01811: } 01812: } 01813: 01814: @Composable ... 01889: Text( 01890: text = "Learn & Print", 01891: fontSize = 24.sp, 01892: fontWeight = FontWeight.Black 01893: ) 01894: 01895: Text( 01896: text = "Create an approval-gated SHiRE mission.", 01897: color = TextMuted, 01898: fontSize = 13.sp 01899: ) 01900: } 01901: }, 01902: text = { 01903: Column( ... 02016: } 02017: }, 02018: containerColor = Surface 02019: ) 02020: } 02021: 02022: @Composable 02023: private fun ApprovalCentreDialog( 02024: approvals: List, 02025: onDismiss: () -> Unit, 02026: onDecision: (Int, String) -> Unit 02027: ) { 02028: var selectedId by remember { 02029: mutableIntStateOf( 02030: approvals.firstOrNull { it.status == "Pending" }?.id 02031: ?: approvals.firstOrNull()?.id 02032: ?: -1 02033: ) 02034: } 02035: 02036: val selected = approvals.firstOrNull { it.id == selectedId } 02037: 02038: AlertDialog( 02039: onDismissRequest = onDismiss, 02040: title = { 02041: Column { 02042: Text( 02043: text = "Approval Centre", 02044: fontSize = 24.sp, 02045: fontWeight = FontWeight.Black 02046: ) 02047: 02048: Text( 02049: text = "${approvals.count { it.status == "Pending" }} pending", 02050: color = Amber 02051: ) 02052: } 02053: }, 02054: text = { 02055: Column( 02056: modifier = Modifier 02057: .heightIn(max = 610.dp) 02058: .verticalScroll(rememberScrollState()) 02059: ) { 02060: approvals.forEach { item -> 02061: FilterChip( 02062: selected = selectedId == item.id, 02063: onClick = { selectedId = item.id }, 02064: label = { 02065: Text("${item.stage}: ${item.title} — ${item.status}") 02066: }, 02067: modifier = Modifier.fillMaxWidth() 02068: ) 02069: } 02070: 02071: selected?.let { item -> 02072: Spacer(modifier = Modifier.height(12.dp)) 02073: 02074: ApprovalDetail("STAGE", item.stage, Amber) 02075: ApprovalDetail("PURPOSE", item.purpose, TextPrimary) 02076: ApprovalDetail("AFFECTS", item.affects, Blue) 02077: ApprovalDetail("RISK", item.risk, Red) 02078: ApprovalDetail("ESTIMATE", item.estimate, Green) 02079: ApprovalDetail("ROLLBACK", item.rollback, TextMuted) 02080: ApprovalDetail("STATUS", item.status, Amber) 02081: 02082: if (item.status == "Pending") { 02083: Text( 02084: text = "Approval applies only to this exact stage.", 02085: color = Green, 02086: fontSize = 12.sp 02087: ) 02088: 02089: Spacer(modifier = Modifier.height(10.dp)) 02090: 02091: FlowRow( ... 02138: } 02139: }, 02140: containerColor = Surface 02141: ) 02142: } 02143: 02144: @Composable 02145: private fun ApprovalDetail( 02146: label: String, 02147: value: String, 02148: valueColor: Color 02149: ) { 02150: SectionLabel(label) 02151: 02152: Text( D. CURRENT 0.7.5 MANIFEST ---------------------------------------------------------------------------- Path: /home/ray/ARMOR/apps/shire-mobile/app/src/main/AndroidManifest.xml E. CURRENT ANDROID BUILD SETTINGS ---------------------------------------------------------------------------- 00035: 00036: android { 00037: namespace = "com.shire.mobile" 00038: compileSdk = 36 00039: 00040: defaultConfig { 00041: applicationId = "com.shire.mobile" 00042: minSdk = 29 00043: targetSdk = 36 00044: versionCode = 13 00045: versionName = "0.7.5-limbforge-ai-guides-live" 00046: buildConfigField( 00047: "String", ... 00093: } 00094: 00095: dependencies { 00096: val composeBom = platform("androidx.compose:compose-bom:2026.06.00") 00097: F. SAFE MERGE CONCLUSIONS ---------------------------------------------------------------------------- Recovered Android component declarations: - class SentinelAlertLinkService : Service() - class SentinelAlertPollJobService : JobService() - class SentinelAlertBootReceiver : BroadcastReceiver() Required merge rules: 1. Introduce SentinelMobileAlerts.kt as a new isolated source file. 2. Preserve current MainActivity.kt and add only the minimum startup/scheduling calls required by the recovered component. 3. Preserve the existing POST_NOTIFICATIONS permission. 4. Add only manifest components that are absent. 5. Do not transplant versionCode/versionName edits from 0.7.3. 6. Do not replace ApprovalInboxActivity notification handling. 7. Keep the existing 0.7.5 LimbForge and AI-guide integration. 8. Validate gateway schema separately before enabling live polling. 9. Build and test before changing any live gateway token state. 10. Preserve the installed 0.7.5 Pixel APK until the new APK passes launch, crash, service, job and self-test checks.