feat: Ipad app production readiness, Colony orchestration, Social posting
All checks were successful
Production Readiness / backend-contracts (pull_request) Successful in 3m19s
Production Readiness / webos-typecheck (pull_request) Successful in 2m38s
Production Readiness / ipad-parse (pull_request) Successful in 1m44s

This commit is contained in:
Sayan Datta
2026-05-03 18:28:04 +05:30
parent acfc602157
commit 6c93e31741
86 changed files with 20349 additions and 1655 deletions

View File

@@ -1,20 +1,36 @@
import SwiftUI
import UIKit
struct SettingsView: View {
@State private var store = AppStore.shared
@State private var session = SessionStore.shared
@State private var ssoProviders: VelocitySSOProvidersDTO?
@State private var mdmConfig: VelocityMDMConfigDTO?
@State private var tenantUsers: [VelocityAuthUserDTO] = []
@State private var identityMessage: String?
@State private var identityError: String?
@State private var isSwitchingSession = false
@State private var isAdvancedConfigurationUnlocked = false
@AppStorage("velocity.notifications.clientInsights") private var clientInsightNotifications = true
@AppStorage("velocity.notifications.calendar") private var calendarNotifications = true
@AppStorage("velocity.notifications.showroom") private var showroomNotifications = false
var body: some View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 4) {
Text("Settings")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Live runtime configuration")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
ScrollView {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 4) {
Text("Settings")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Profile and notification preferences")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
profileSection
notificationPreferencesSection
if isAdvancedConfigurationUnlocked {
SettingsSection(title: "Connectivity") {
SettingsRow(
label: "Backend endpoint",
@@ -96,6 +112,81 @@ struct SettingsView: View {
)
}
SettingsSection(title: "Enterprise Identity") {
SettingsRow(
label: "SSO providers",
value: ssoProviders?.providers.map(\.name).joined(separator: ", ") ?? "Not loaded",
icon: "person.badge.key",
accentColor: ssoProviders?.enabled == true ? VelocityTheme.success : VelocityTheme.warning
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "MDM configuration",
value: mdmConfig?.managedConfigurationRequired == true ? "Required · \(mdmConfig?.configurationKeys.count ?? 0) keys" : "Optional · \(mdmConfig?.configurationKeys.count ?? 0) keys",
icon: "iphone.badge.gearshape",
accentColor: mdmConfig == nil ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Tenant users",
value: tenantUsers.isEmpty ? "Not loaded" : "\(tenantUsers.count) available",
icon: "person.2.badge.gearshape",
accentColor: tenantUsers.isEmpty ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
Button {
Task { await requestPasswordRecovery() }
} label: {
Label("Request Recovery", systemImage: "lock.rotation")
}
.buttonStyle(.borderedProminent)
.tint(VelocityTheme.accent)
Button {
Task { await loadEnterpriseIdentity() }
} label: {
Label("Refresh Identity", systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
.tint(VelocityTheme.accent)
Menu {
if tenantUsers.isEmpty {
Text("No tenant users returned")
} else {
ForEach(tenantUsers) { user in
Button {
Task { await switchSession(to: user) }
} label: {
Label("\(user.displayName) · \(user.role)", systemImage: "person.crop.circle.badge.checkmark")
}
}
}
} label: {
Label(isSwitchingSession ? "Switching..." : "Switch User", systemImage: "person.2")
}
.buttonStyle(.bordered)
.tint(VelocityTheme.accent)
.disabled(isSwitchingSession || tenantUsers.isEmpty)
}
if let identityMessage {
Text(identityMessage)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.success)
}
if let identityError {
Text(identityError)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.danger)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
SettingsSection(title: "Production Readiness") {
SettingsRow(
label: "Canonical contacts",
@@ -145,19 +236,151 @@ struct SettingsView: View {
Text("This build avoids local demo data. Runtime session overrides are stored on-device so investor or operator installs no longer depend on committed build-time credentials.")
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.foreground)
Text("\(SentinelScope.navigationTitle) remains the truthful iPad label for the current \(SentinelScope.productFamilyName) surface because visitor analytics stay disabled until a dedicated production feed exists; Communications, Calendar, Dashboard, Oracle pipeline, and inventory summaries are live-backed. Dream Weaver can now use a dedicated gateway with an optional per-gateway API key, and backend plus generation route health are still enforced and reported truthfully.")
Text("\(SentinelScope.navigationTitle) now reads persisted perception analytics from the production Sentinel stream; Communications, Calendar, Dashboard, Oracle, and inventory media are live-backed. Dream Weaver can use a dedicated gateway with an optional per-gateway API key, and backend plus generation route health are enforced and reported truthfully.")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
}
}
Spacer()
Spacer(minLength: 24)
}
.padding(24)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.padding(24)
.overlay(alignment: .topTrailing) {
ThreeFingerLongPressGate {
withAnimation(.interactiveSpring(response: 0.45, dampingFraction: 0.86)) {
isAdvancedConfigurationUnlocked = true
}
}
.frame(width: 180, height: 180)
.allowsHitTesting(!isAdvancedConfigurationUnlocked)
}
.scrollIndicators(.visible)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(VelocityTheme.background)
.task {
if isAdvancedConfigurationUnlocked {
await loadEnterpriseIdentity()
}
}
.onChange(of: isAdvancedConfigurationUnlocked) { _, unlocked in
guard unlocked else { return }
Task { await loadEnterpriseIdentity() }
}
}
private var profileSection: some View {
SettingsSection(title: "Profile") {
SettingsRow(
label: "Signed in",
value: session.operatorIdentity,
icon: "person.crop.circle",
accentColor: VelocityTheme.accent
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Session",
value: session.authModeDescription,
icon: "lock.shield",
accentColor: session.isConfigured ? VelocityTheme.success : VelocityTheme.warning
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Showroom privacy",
value: store.isShowroomModeEnabled ? "Buyer-safe" : "Broker private",
icon: store.isShowroomModeEnabled ? "eye.slash" : "eye",
accentColor: store.isShowroomModeEnabled ? VelocityTheme.warning : VelocityTheme.success
)
}
}
private var notificationPreferencesSection: some View {
SettingsSection(title: "Notifications") {
ToggleRow(
label: "Client insight alerts",
detail: "Private broker recommendations",
icon: "sparkles",
accentColor: VelocityTheme.accent,
isOn: $clientInsightNotifications
)
Divider().background(VelocityTheme.borderSubtle)
ToggleRow(
label: "Calendar reminders",
detail: "Confirmed events and follow-ups",
icon: "calendar.badge.clock",
accentColor: VelocityTheme.warning,
isOn: $calendarNotifications
)
Divider().background(VelocityTheme.borderSubtle)
ToggleRow(
label: "Showroom mode changes",
detail: "Buyer-safe privacy transitions",
icon: "eye.slash",
accentColor: VelocityTheme.success,
isOn: $showroomNotifications
)
}
}
private func loadEnterpriseIdentity() async {
do {
async let providers = VelocityAPIClient.shared.fetchSSOProviders()
async let mdm = VelocityAPIClient.shared.fetchMDMConfig()
async let users = VelocityAPIClient.shared.fetchAuthUsers()
let resolvedProviders = try await providers
let resolvedMDM = try await mdm
let resolvedUsers = (try? await users) ?? []
await MainActor.run {
ssoProviders = resolvedProviders
mdmConfig = resolvedMDM
tenantUsers = resolvedUsers
identityError = nil
}
} catch {
await MainActor.run { identityError = error.localizedDescription }
}
}
private func requestPasswordRecovery() async {
do {
try await VelocityAPIClient.shared.requestPasswordRecovery(email: session.operatorIdentity)
await MainActor.run {
identityMessage = "Password recovery request recorded for \(session.operatorIdentity)."
identityError = nil
}
} catch {
await MainActor.run {
identityError = error.localizedDescription
identityMessage = nil
}
}
}
private func switchSession(to user: VelocityAuthUserDTO) async {
await MainActor.run {
isSwitchingSession = true
identityError = nil
identityMessage = nil
}
do {
let result = try await VelocityAPIClient.shared.requestSessionSwitch(userId: user.userId)
await store.refresh(silent: true)
await MainActor.run {
isSwitchingSession = false
identityMessage = result.requiresReauthentication
? "Session switch approved for \(user.displayName); reauthentication is required."
: "Session switched to \(user.displayName)."
}
} catch {
await MainActor.run {
isSwitchingSession = false
identityError = error.localizedDescription
}
}
}
}
@@ -222,3 +445,76 @@ private struct SettingsRow: View {
.padding(.vertical, 12)
}
}
private struct ToggleRow: View {
let label: String
let detail: String
let icon: String
let accentColor: Color
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
HStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(accentColor.opacity(0.12))
.frame(width: 30, height: 30)
Image(systemName: icon)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(accentColor)
}
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
Text(detail)
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
}
}
}
.toggleStyle(.switch)
.tint(VelocityTheme.accent)
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
private struct ThreeFingerLongPressGate: UIViewRepresentable {
let onUnlock: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let recognizer = UILongPressGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.didLongPress(_:))
)
recognizer.minimumPressDuration = 1.15
recognizer.numberOfTouchesRequired = 3
view.addGestureRecognizer(recognizer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onUnlock: onUnlock)
}
final class Coordinator: NSObject {
let onUnlock: () -> Void
init(onUnlock: @escaping () -> Void) {
self.onUnlock = onUnlock
}
@objc func didLongPress(_ recognizer: UILongPressGestureRecognizer) {
guard recognizer.state == .began else { return }
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
onUnlock()
}
}
}