import SwiftUI // Tamper-evidence failure banner (docs/tamper-evidence.md, #573): the native // twin of the web's #integrity-banner. Shown ONLY when this space fails an // integrity check (a fork/rollback/withheld/bad-signature the E2E rule is meant // to make un-hideable) — never for "all clear" (a pulled /integrity self-check // covers that). An inline band occupying layout, never a floating overlay; // amber with a ⚠️ so it's never colour-alone. `onDetails` runs /integrity so // the full read-out shows in place. public struct IntegrityBanner: View { @ObservedObject var monitor: IntegrityMonitor let onDetails: () -> Void public init(monitor: IntegrityMonitor, onDetails: @escaping () -> Void) { self.monitor = monitor self.onDetails = onDetails } private var title: String { let n = monitor.unresolved.count return n > 1 ? "Integrity check failed (\(n.formatted()) alerts)" : "Integrity check failed" } private var message: String { guard let latest = monitor.unresolved.last else { return "" } return IntegrityMonitor.alertMessage(latest.kind) + " Keep this device — its records are the evidence — and contact your space's admin." } public var body: some View { if !monitor.unresolved.isEmpty { HStack(alignment: .top, spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.white) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 4) { Text(title).font(.subheadline).bold() Text(message).font(.footnote) HStack(spacing: 8) { Button("What's this?") { onDetails() } .buttonStyle(.borderedProminent) Button("Dismiss") { monitor.acknowledge() } .buttonStyle(.bordered) } .controlSize(.small) .padding(.top, 2) } Spacer(minLength: 0) } .foregroundStyle(.white) .tint(.white) .padding(.horizontal, 14) .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) .background(Color(red: 0.72, green: 0.42, blue: 0.05)) // amber, holds in light + dark .accessibilityElement(children: .contain) .accessibilityLabel("Integrity check failed. \(message)") } } }