forked from sagnik/Project_Velocity
Merge Conflicts (#41)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#41
This commit is contained in:
209
iOS/velocity-ipad/velocity/Features/Sentinel/SentinelView.swift
Normal file
209
iOS/velocity-ipad/velocity/Features/Sentinel/SentinelView.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct SentinelView: View {
|
||||
@State private var store = AppStore.shared
|
||||
private let refreshTimer = Timer.publish(every: 20, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
header
|
||||
|
||||
if let error = store.errorMessage {
|
||||
errorBanner(error)
|
||||
}
|
||||
|
||||
availabilityCard
|
||||
postureCards
|
||||
timelineCard
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.background(VelocityTheme.background)
|
||||
.scrollContentBackground(.hidden)
|
||||
.task { await store.refresh() }
|
||||
.refreshable { await store.refresh() }
|
||||
.onReceive(refreshTimer) { _ in
|
||||
Task { await store.refresh(silent: true) }
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(SentinelScope.productFamilyName.uppercased())
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.tracking(1.2)
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
Text(SentinelScope.navigationTitle)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Text("Truthful live posture for alerts and comms load. \(SentinelScope.productFamilyName) analytics stay disabled on iPad until a real production stream is exposed.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
}
|
||||
|
||||
private var availabilityCard: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Production Scope")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Spacer()
|
||||
statusBadge(
|
||||
label: SentinelScope.availabilityBadge,
|
||||
color: VelocityTheme.warning
|
||||
)
|
||||
}
|
||||
|
||||
Text("This iPad build does not synthesize \(SentinelScope.disabledAnalyticsSummary). A dedicated production Sentinel route is still required before those analytics can be shown safely.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
|
||||
Text("Current surface instead reports real \(SentinelScope.liveBackedSummary) from the live mobile-edge backend.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
.padding(20)
|
||||
.glassCard(cornerRadius: 18)
|
||||
}
|
||||
|
||||
private var postureCards: some View {
|
||||
HStack(spacing: 14) {
|
||||
SentinelCard(
|
||||
title: "Pending insights",
|
||||
value: "\(store.metrics.pendingInsights)",
|
||||
subtitle: "Recommendations waiting on operator review",
|
||||
color: VelocityTheme.danger
|
||||
)
|
||||
SentinelCard(
|
||||
title: "Transcript queue",
|
||||
value: "\(store.metrics.pendingTranscriptions)",
|
||||
subtitle: "Imported recordings still processing",
|
||||
color: VelocityTheme.warning
|
||||
)
|
||||
SentinelCard(
|
||||
title: "Upcoming 24h",
|
||||
value: "\(store.alertSnapshot?.upcomingCalendarEvents24h ?? 0)",
|
||||
subtitle: "Calendar events due soon",
|
||||
color: VelocityTheme.success
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var timelineCard: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("Recent Operator Timeline")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Spacer()
|
||||
if let lastRefresh = store.lastRefreshAt {
|
||||
Text("Updated \(lastRefresh.relativeShort)")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
}
|
||||
|
||||
if store.timelineEvents.isEmpty {
|
||||
Text("No live communication events have been loaded for the current high-priority leads yet.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
} else {
|
||||
ForEach(store.timelineEvents.prefix(6)) { item in
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(item.leadName)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Spacer()
|
||||
Text(item.event.channel.replacingOccurrences(of: "_", with: " ").capitalized)
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundStyle(VelocityTheme.accent)
|
||||
}
|
||||
Text(item.event.summary ?? "No summary available for this event.")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
Text(item.event.timestampDate?.relativeShort ?? item.event.timestamp)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
}
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(VelocityTheme.surface)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.glassCard(cornerRadius: 18)
|
||||
}
|
||||
|
||||
private func statusBadge(label: String, color: Color) -> some View {
|
||||
Text(label)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(color.opacity(0.12))
|
||||
.overlay(Capsule().stroke(color.opacity(0.22), lineWidth: 1))
|
||||
)
|
||||
}
|
||||
|
||||
private func errorBanner(_ message: String) -> some View {
|
||||
Text(message)
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(VelocityTheme.danger)
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(VelocityTheme.danger.opacity(0.10))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.stroke(VelocityTheme.danger.opacity(0.22), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SentinelCard: View {
|
||||
let title: String
|
||||
let value: String
|
||||
let subtitle: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(title.uppercased())
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.tracking(1)
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
Text(value)
|
||||
.font(.system(size: 26, weight: .bold))
|
||||
.foregroundStyle(VelocityTheme.foreground)
|
||||
Text(subtitle)
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(VelocityTheme.mutedFg)
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(color)
|
||||
.frame(width: 48, height: 4)
|
||||
}
|
||||
.padding(18)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.glassCard(cornerRadius: 16)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SentinelView()
|
||||
}
|
||||
Reference in New Issue
Block a user