Merge Conflicts (#41)
Some checks failed
Production Readiness / backend-contracts (push) Failing after 1m47s
Production Readiness / webos-typecheck (push) Successful in 1m57s
Production Readiness / ipad-parse (push) Successful in 1m32s

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
2026-04-28 11:32:56 +05:30
parent 61258978e1
commit 7ee51543d9
158 changed files with 23889 additions and 87196 deletions

View File

@@ -0,0 +1,267 @@
import SwiftUI
struct SessionConfigurationPanel: View {
@State private var session = SessionStore.shared
let title: String
let subtitle: String
let primaryActionTitle: String
let allowsClearingStoredConfiguration: Bool
var body: some View {
@Bindable var session = session
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Text(subtitle)
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
VStack(spacing: 14) {
SessionInputField(
label: "Backend endpoint",
placeholder: "https://velocity.desineuron.in/api"
) {
TextField("", text: $session.draftBaseURL, prompt: Text("https://velocity.desineuron.in/api"))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
}
SessionInputField(
label: "Dream Weaver endpoint",
placeholder: "Leave blank to use the backend endpoint"
) {
TextField("", text: $session.draftDreamWeaverBaseURL, prompt: Text("https://dreamweaver.desineuron.in"))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
}
SessionInputField(
label: "Dream Weaver gateway API key",
placeholder: session.currentConfiguration.hasDreamWeaverAPIKey ? "Leave blank to keep the current gateway key" : "Optional unless the gateway enforces it"
) {
SecureField(
"",
text: $session.draftDreamWeaverAPIKey,
prompt: Text(session.currentConfiguration.hasDreamWeaverAPIKey ? "Leave blank to keep the current gateway key" : "Optional unless the gateway enforces it")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
VStack(alignment: .leading, spacing: 8) {
Text("Authentication mode")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.mutedFg)
Picker("Authentication mode", selection: $session.draftAuthMode) {
ForEach(SessionAuthMode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.segmented)
}
if session.draftAuthMode == .emailPassword {
SessionInputField(
label: "Operator email",
placeholder: "operator@desineuron.in"
) {
TextField("", text: $session.draftEmail, prompt: Text("operator@desineuron.in"))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
}
SessionInputField(
label: "Password",
placeholder: session.currentConfiguration.hasPassword ? "Leave blank to keep the current secret" : "Required"
) {
SecureField(
"",
text: $session.draftPassword,
prompt: Text(session.currentConfiguration.hasPassword ? "Leave blank to keep the current secret" : "Required")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
} else {
SessionInputField(
label: "Bearer token",
placeholder: session.currentConfiguration.hasBearerToken ? "Leave blank to keep the current token" : "Required"
) {
SecureField(
"",
text: $session.draftBearerToken,
prompt: Text(session.currentConfiguration.hasBearerToken ? "Leave blank to keep the current token" : "Required")
)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Current source: \(session.configurationSourceDescription)")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.foreground)
Text("Runtime overrides are saved on-device. Secrets are stored in Keychain; the backend endpoint, optional Dream Weaver endpoint, and operator email are stored in local app preferences.")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
}
if let message = session.statusMessage {
SessionStatusBanner(message: message, accentColor: VelocityTheme.success)
}
if let error = session.errorMessage {
SessionStatusBanner(message: error, accentColor: VelocityTheme.danger)
}
HStack(spacing: 12) {
Button {
Task { await session.saveDraft() }
} label: {
HStack(spacing: 8) {
if session.isSaving {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.tint(.white)
}
Text(primaryActionTitle)
.font(.system(size: 14, weight: .semibold))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
}
.buttonStyle(SessionActionButtonStyle(background: VelocityTheme.accent))
.disabled(session.isSaving)
Button("Reset form") {
session.discardDraftChanges()
}
.buttonStyle(SessionSecondaryButtonStyle())
.disabled(session.isSaving || !session.hasUnsavedChanges)
}
if allowsClearingStoredConfiguration {
Button("Clear stored session override") {
Task { await session.clearStoredConfiguration() }
}
.buttonStyle(SessionDangerButtonStyle())
.disabled(session.isSaving || !session.isUsingStoredRuntimeConfiguration)
}
}
.padding(20)
.glassCard(cornerRadius: 20)
}
}
private struct SessionInputField<Field: View>: View {
let label: String
let placeholder: String
@ViewBuilder let field: Field
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(label)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.mutedFg)
field
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.surface)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
Text(placeholder)
.font(.system(size: 10))
.foregroundStyle(VelocityTheme.mutedFg.opacity(0.9))
}
}
}
private struct SessionStatusBanner: View {
let message: String
let accentColor: Color
var body: some View {
Text(message)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(accentColor)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(accentColor.opacity(0.10))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(accentColor.opacity(0.18), lineWidth: 1)
)
)
}
}
private struct SessionActionButtonStyle: ButtonStyle {
let background: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(.white)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(background.opacity(configuration.isPressed ? 0.82 : 1))
)
}
}
private struct SessionSecondaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 14, weight: .medium))
.foregroundStyle(VelocityTheme.foreground)
.padding(.horizontal, 18)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.surface.opacity(configuration.isPressed ? 0.78 : 1))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
}
}
private struct SessionDangerButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 13, weight: .medium))
.foregroundStyle(VelocityTheme.danger)
.padding(.horizontal, 14)
.padding(.vertical, 11)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(VelocityTheme.danger.opacity(configuration.isPressed ? 0.14 : 0.10))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(VelocityTheme.danger.opacity(0.2), lineWidth: 1)
)
)
}
}

View File

@@ -0,0 +1,224 @@
import SwiftUI
struct SettingsView: View {
@State private var store = AppStore.shared
@State private var session = SessionStore.shared
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)
}
SettingsSection(title: "Connectivity") {
SettingsRow(
label: "Backend endpoint",
value: session.endpointDisplay,
icon: "server.rack",
accentColor: VelocityTheme.accent
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Dream Weaver endpoint",
value: session.dreamWeaverEndpointDisplay,
icon: "wand.and.stars",
accentColor: session.dreamWeaverEndpointModeDescription == "Dedicated gateway" ? VelocityTheme.warning : VelocityTheme.mutedFg
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Dream Weaver route mode",
value: session.dreamWeaverEndpointModeDescription,
icon: "point.3.connected.trianglepath.dotted",
accentColor: session.dreamWeaverEndpointModeDescription == "Dedicated gateway" ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Dream Weaver auth",
value: session.dreamWeaverAuthenticationDescription,
icon: "key.horizontal",
accentColor: session.dreamWeaverAuthenticationDescription == "API key configured" ? VelocityTheme.success : VelocityTheme.mutedFg
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Auth mode",
value: session.authModeDescription,
icon: "lock.shield",
accentColor: session.isConfigured ? VelocityTheme.success : VelocityTheme.warning
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Config source",
value: session.configurationSourceDescription,
icon: "externaldrive.badge.icloud",
accentColor: session.isUsingStoredRuntimeConfiguration ? VelocityTheme.success : VelocityTheme.mutedFg
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Last refresh",
value: store.lastRefreshAt?.relativeShort ?? "No live fetch yet",
icon: "arrow.clockwise",
accentColor: VelocityTheme.mutedFg
)
}
SettingsSection(title: "Operator") {
SettingsRow(
label: "Identity",
value: session.operatorIdentity,
icon: "person.crop.circle",
accentColor: VelocityTheme.accent
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "CRM contacts loaded",
value: "\(store.contacts.count)",
icon: "person.3",
accentColor: VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Pending CRM tasks loaded",
value: "\(store.tasks.count)",
icon: "checklist",
accentColor: VelocityTheme.warning
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Property records loaded",
value: "\(store.properties.count)",
icon: "building.2",
accentColor: VelocityTheme.warning
)
}
SettingsSection(title: "Production Readiness") {
SettingsRow(
label: "Canonical contacts",
value: "\(store.contacts.count) loaded",
icon: "person.text.rectangle",
accentColor: store.contacts.isEmpty ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Pipeline lanes",
value: "\(store.kanbanColumns.reduce(0) { $0 + $1.count }) leads",
icon: "square.grid.3x1.below.line.grid.1x2",
accentColor: store.kanbanColumns.isEmpty ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Deals",
value: "\(store.opportunities.count) opportunities",
icon: "target",
accentColor: store.opportunities.isEmpty ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Timeline events",
value: "\(store.timelineEvents.count) hydrated",
icon: "clock.arrow.circlepath",
accentColor: store.timelineEvents.isEmpty ? VelocityTheme.warning : VelocityTheme.success
)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(
label: "Last app error",
value: store.errorMessage ?? "None",
icon: "exclamationmark.triangle",
accentColor: store.errorMessage == nil ? VelocityTheme.success : VelocityTheme.danger
)
}
SessionConfigurationPanel(
title: "Session Configuration",
subtitle: "Update the production endpoint, point Dream Weaver at a dedicated gateway when needed, or rotate operator credentials without rebuilding the app. Saving clears the cached token, re-runs a live refresh, and probes the Dream Weaver routes.",
primaryActionTitle: "Save and refresh",
allowsClearingStoredConfiguration: true
)
SettingsSection(title: "Production Notes") {
VStack(alignment: .leading, spacing: 8) {
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.")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
}
Spacer()
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(VelocityTheme.background)
}
}
private struct SettingsSection<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title.uppercased())
.font(.system(size: 10, weight: .semibold))
.tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg)
.padding(.bottom, 8)
.padding(.horizontal, 4)
VStack(spacing: 0) {
content
}
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
}
}
}
private struct SettingsRow: View {
let label: String
let value: String
let icon: String
let accentColor: Color
var body: some View {
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)
}
Text(label)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
Spacer()
Text(value)
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.mutedFg)
.multilineTextAlignment(.trailing)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}