444 lines
17 KiB
Swift
444 lines
17 KiB
Swift
import SwiftUI
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// NTS Anchor View — 36 Global Atomic Time Servers
|
||
// Montana Protocol v3.17.0
|
||
//
|
||
// Визуализация NTS-якорей: каждое τ₂ окно привязано к глобальному
|
||
// атомному времени через TLS 1.3 handshake с 36 серверами (RFC 8915).
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
struct NTSAnchorView: View {
|
||
@EnvironmentObject var engine: PresenceEngine
|
||
@Environment(\.dismiss) private var dismiss
|
||
@State private var showSidebar = false
|
||
|
||
// Data
|
||
@State private var ntsStatus: [String: Any] = [:]
|
||
@State private var serverList: [[String: Any]] = []
|
||
@State private var regions: [String: Any] = [:]
|
||
@State private var latestAnchor: [String: Any]? = nil
|
||
@State private var attestations: [[String: Any]] = []
|
||
@State private var isLoading = true
|
||
@State private var errorText = ""
|
||
@State private var selectedRegion: String? = nil
|
||
|
||
// Colors
|
||
private let cyan = Color(red: 0, green: 0.83, blue: 1)
|
||
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
|
||
private let cardBg = Color(red: 0.09, green: 0.09, blue: 0.12)
|
||
private let green = Color(red: 0.2, green: 0.85, blue: 0.4)
|
||
|
||
// Region display order
|
||
private let regionOrder = ["GLOBAL", "EU", "RU", "US", "SA", "ASIA", "OCEANIA", "POLES"]
|
||
private let regionEmoji: [String: String] = [
|
||
"GLOBAL": "\u{1F310}", "EU": "\u{1F1EA}\u{1F1FA}", "RU": "\u{1F1F7}\u{1F1FA}",
|
||
"US": "\u{1F1FA}\u{1F1F8}", "SA": "\u{1F30E}", "ASIA": "\u{1F30F}",
|
||
"OCEANIA": "\u{1F30F}", "POLES": "\u{2744}\u{FE0F}"
|
||
]
|
||
private let regionNames: [String: String] = [
|
||
"GLOBAL": "Global", "EU": "Europe", "RU": "Russia",
|
||
"US": "USA", "SA": "South America", "ASIA": "Asia",
|
||
"OCEANIA": "Oceania", "POLES": "Poles"
|
||
]
|
||
|
||
var body: some View {
|
||
ZStack(alignment: .leading) {
|
||
VStack(spacing: 0) {
|
||
// Burger menu
|
||
HStack {
|
||
Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) {
|
||
Image(systemName: "line.3.horizontal")
|
||
.font(.system(size: 18, weight: .medium))
|
||
.foregroundColor(gold)
|
||
.padding(8)
|
||
}
|
||
.buttonStyle(.plain)
|
||
Spacer()
|
||
}
|
||
.padding(.horizontal, 20)
|
||
.padding(.top, 24)
|
||
|
||
// Header
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "shield.checkered")
|
||
.foregroundColor(cyan)
|
||
.font(.system(size: 16))
|
||
Text("NTS Anchor")
|
||
.font(.system(size: 14, weight: .bold))
|
||
Text("36 Atomic Servers")
|
||
.font(.system(size: 10))
|
||
.foregroundColor(.secondary)
|
||
Spacer()
|
||
Button(action: { loadData() }) {
|
||
Image(systemName: "arrow.clockwise")
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.buttonStyle(.plain)
|
||
Button(action: { dismiss() }) {
|
||
Image(systemName: "xmark.circle.fill")
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
.padding(.horizontal, 20)
|
||
.padding(.top, 4)
|
||
.padding(.bottom, 8)
|
||
|
||
Divider()
|
||
|
||
if isLoading {
|
||
Spacer()
|
||
ProgressView().controlSize(.small)
|
||
Text("Loading NTS status...")
|
||
.font(.caption).foregroundColor(.secondary)
|
||
Spacer()
|
||
} else if !errorText.isEmpty {
|
||
Spacer()
|
||
VStack(spacing: 8) {
|
||
Image(systemName: "exclamationmark.triangle")
|
||
.font(.system(size: 24)).foregroundColor(.orange)
|
||
Text(errorText)
|
||
.font(.caption).foregroundColor(.red)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
Spacer()
|
||
} else {
|
||
ScrollView {
|
||
VStack(spacing: 12) {
|
||
// Status card
|
||
statusCard
|
||
|
||
// Region grid
|
||
regionGrid
|
||
|
||
// Latest anchor details
|
||
if let anchor = latestAnchor {
|
||
anchorDetailCard(anchor)
|
||
}
|
||
|
||
// Server list
|
||
serverListSection
|
||
}
|
||
.padding(.horizontal, 20)
|
||
.padding(.top, 12)
|
||
.padding(.bottom, 20)
|
||
}
|
||
}
|
||
}
|
||
.background(Color.black)
|
||
|
||
// Sidebar
|
||
if showSidebar {
|
||
SharedSidebar(isVisible: $showSidebar)
|
||
.environmentObject(engine)
|
||
}
|
||
}
|
||
.onAppear { loadData() }
|
||
}
|
||
|
||
// ─── Status Card ──────────────────────────────────────────────
|
||
|
||
private var statusCard: some View {
|
||
VStack(spacing: 8) {
|
||
HStack {
|
||
let enabled = ntsStatus["nts_enabled"] as? Bool ?? false
|
||
Circle()
|
||
.fill(enabled ? green : .red)
|
||
.frame(width: 8, height: 8)
|
||
Text(enabled ? "NTS Active" : "NTS Disabled")
|
||
.font(.system(size: 11, weight: .bold))
|
||
.foregroundColor(enabled ? green : .red)
|
||
Spacer()
|
||
let coverage = ntsStatus["coverage"] as? String ?? "0/0"
|
||
Text(coverage)
|
||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
||
.foregroundColor(gold)
|
||
Text("anchored")
|
||
.font(.system(size: 9))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
HStack(spacing: 16) {
|
||
statPill(
|
||
label: "Servers",
|
||
value: "\(serverList.count)",
|
||
icon: "server.rack"
|
||
)
|
||
statPill(
|
||
label: "Regions",
|
||
value: "\(regions.count)",
|
||
icon: "globe"
|
||
)
|
||
let anchors = ntsStatus["total_anchors"] as? Int ?? 0
|
||
statPill(
|
||
label: "Anchors",
|
||
value: "\(anchors)",
|
||
icon: "lock.shield"
|
||
)
|
||
}
|
||
}
|
||
.padding(12)
|
||
.background(cardBg)
|
||
.cornerRadius(10)
|
||
}
|
||
|
||
private func statPill(label: String, value: String, icon: String) -> some View {
|
||
HStack(spacing: 4) {
|
||
Image(systemName: icon)
|
||
.font(.system(size: 9))
|
||
.foregroundColor(cyan)
|
||
Text(value)
|
||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||
Text(label)
|
||
.font(.system(size: 9))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
|
||
// ─── Region Grid ──────────────────────────────────────────────
|
||
|
||
private var regionGrid: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text("REGIONS")
|
||
.font(.system(size: 9, weight: .bold))
|
||
.foregroundColor(.secondary)
|
||
|
||
LazyVGrid(columns: [
|
||
GridItem(.flexible()), GridItem(.flexible()),
|
||
GridItem(.flexible()), GridItem(.flexible())
|
||
], spacing: 6) {
|
||
ForEach(regionOrder, id: \.self) { region in
|
||
let count = serverCountForRegion(region)
|
||
if count > 0 {
|
||
regionCell(region: region, count: count)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func regionCell(region: String, count: Int) -> some View {
|
||
let isSelected = selectedRegion == region
|
||
return VStack(spacing: 2) {
|
||
Text(regionEmoji[region] ?? "")
|
||
.font(.system(size: 16))
|
||
Text("\(count)")
|
||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||
Text(regionNames[region] ?? region)
|
||
.font(.system(size: 8))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.vertical, 6)
|
||
.background(isSelected ? cyan.opacity(0.15) : cardBg)
|
||
.cornerRadius(8)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.stroke(isSelected ? cyan : Color.clear, lineWidth: 1)
|
||
)
|
||
.onTapGesture {
|
||
withAnimation(.easeInOut(duration: 0.2)) {
|
||
selectedRegion = selectedRegion == region ? nil : region
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Anchor Detail Card ───────────────────────────────────────
|
||
|
||
private func anchorDetailCard(_ anchor: [String: Any]) -> some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
HStack {
|
||
Image(systemName: "lock.shield.fill")
|
||
.foregroundColor(gold)
|
||
Text("Latest NTS Anchor")
|
||
.font(.system(size: 11, weight: .bold))
|
||
Spacer()
|
||
}
|
||
|
||
if let contentHash = anchor["content_hash"] as? String {
|
||
hashRow(label: "Content Hash", hash: contentHash)
|
||
}
|
||
if let anchorHash = anchor["anchor_hash"] as? String {
|
||
hashRow(label: "Anchor Hash", hash: anchorHash)
|
||
}
|
||
|
||
HStack(spacing: 12) {
|
||
if let servers = anchor["server_count"] as? Int {
|
||
detailPill(icon: "server.rack", value: "\(servers)", label: "servers")
|
||
}
|
||
if let regionCount = anchor["region_count"] as? Int {
|
||
detailPill(icon: "globe", value: "\(regionCount)", label: "regions")
|
||
}
|
||
if let spreadNs = anchor["timestamp_spread_ns"] as? Int {
|
||
let spreadMs = Double(spreadNs) / 1_000_000.0
|
||
detailPill(icon: "clock", value: String(format: "%.1fms", spreadMs), label: "spread")
|
||
}
|
||
}
|
||
|
||
// Attestation list
|
||
if let atts = anchor["attestations"] as? [[String: Any]], !atts.isEmpty {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text("ATTESTATIONS (\(atts.count))")
|
||
.font(.system(size: 8, weight: .bold))
|
||
.foregroundColor(.secondary)
|
||
.padding(.top, 4)
|
||
|
||
ForEach(Array(atts.prefix(12).enumerated()), id: \.offset) { _, att in
|
||
attestationRow(att)
|
||
}
|
||
if atts.count > 12 {
|
||
Text("+ \(atts.count - 12) more...")
|
||
.font(.system(size: 9))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(12)
|
||
.background(cardBg)
|
||
.cornerRadius(10)
|
||
}
|
||
|
||
private func attestationRow(_ att: [String: Any]) -> some View {
|
||
let host = att["server_host"] as? String ?? "?"
|
||
let region = att["server_region"] as? String ?? ""
|
||
let tls = att["tls_version"] as? String ?? ""
|
||
let cert = att["cert_fingerprint"] as? String ?? ""
|
||
|
||
return HStack(spacing: 4) {
|
||
Circle().fill(green).frame(width: 4, height: 4)
|
||
Text(regionEmoji[region] ?? "")
|
||
.font(.system(size: 8))
|
||
Text(host)
|
||
.font(.system(size: 9, design: .monospaced))
|
||
.foregroundColor(.white)
|
||
.lineLimit(1)
|
||
Spacer()
|
||
Text(tls)
|
||
.font(.system(size: 8))
|
||
.foregroundColor(tls.contains("1.3") ? green : .orange)
|
||
Text(String(cert.prefix(8)))
|
||
.font(.system(size: 8, design: .monospaced))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.padding(.vertical, 1)
|
||
}
|
||
|
||
private func hashRow(label: String, hash: String) -> some View {
|
||
HStack {
|
||
Text(label)
|
||
.font(.system(size: 9))
|
||
.foregroundColor(.secondary)
|
||
Spacer()
|
||
Text(String(hash.prefix(16)) + "...")
|
||
.font(.system(size: 9, design: .monospaced))
|
||
.foregroundColor(cyan)
|
||
}
|
||
}
|
||
|
||
private func detailPill(icon: String, value: String, label: String) -> some View {
|
||
HStack(spacing: 3) {
|
||
Image(systemName: icon)
|
||
.font(.system(size: 8))
|
||
.foregroundColor(gold)
|
||
Text(value)
|
||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||
Text(label)
|
||
.font(.system(size: 8))
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
|
||
// ─── Server List ──────────────────────────────────────────────
|
||
|
||
private var serverListSection: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text("ALL SERVERS (\(filteredServers.count))")
|
||
.font(.system(size: 9, weight: .bold))
|
||
.foregroundColor(.secondary)
|
||
|
||
ForEach(Array(filteredServers.enumerated()), id: \.offset) { idx, server in
|
||
serverRow(server, index: idx + 1)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var filteredServers: [[String: Any]] {
|
||
if let region = selectedRegion {
|
||
return serverList.filter { ($0["region"] as? String) == region }
|
||
}
|
||
return serverList
|
||
}
|
||
|
||
private func serverRow(_ server: [String: Any], index: Int) -> some View {
|
||
let host = server["host"] as? String ?? "?"
|
||
let region = server["region"] as? String ?? ""
|
||
let org = server["org"] as? String ?? ""
|
||
let country = server["country"] as? String ?? ""
|
||
let port = server["port"] as? Int ?? 4460
|
||
|
||
return HStack(spacing: 6) {
|
||
Text("\(index)")
|
||
.font(.system(size: 8, design: .monospaced))
|
||
.foregroundColor(.secondary)
|
||
.frame(width: 18, alignment: .trailing)
|
||
Text(country)
|
||
.font(.system(size: 10))
|
||
VStack(alignment: .leading, spacing: 1) {
|
||
Text(host)
|
||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||
.foregroundColor(.white)
|
||
.lineLimit(1)
|
||
Text("\(org) : \(port)")
|
||
.font(.system(size: 8))
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
Spacer()
|
||
Text(regionNames[region] ?? region)
|
||
.font(.system(size: 8))
|
||
.foregroundColor(cyan)
|
||
.padding(.horizontal, 4)
|
||
.padding(.vertical, 1)
|
||
.background(cyan.opacity(0.1))
|
||
.cornerRadius(3)
|
||
}
|
||
.padding(.vertical, 3)
|
||
.padding(.horizontal, 8)
|
||
.background(cardBg)
|
||
.cornerRadius(6)
|
||
}
|
||
|
||
// ─── Data Loading ─────────────────────────────────────────────
|
||
|
||
private func loadData() {
|
||
isLoading = true
|
||
errorText = ""
|
||
Task { @MainActor in
|
||
do {
|
||
async let fetchServers = engine.api.fetchNTSServers()
|
||
async let fetchStatus = engine.api.fetchNTSStatus()
|
||
|
||
let serversResult = try await fetchServers
|
||
let statusResult = try await fetchStatus
|
||
|
||
serverList = serversResult["servers"] as? [[String: Any]] ?? []
|
||
regions = serversResult["regions"] as? [String: Any] ?? [:]
|
||
ntsStatus = statusResult
|
||
latestAnchor = statusResult["latest_anchor"] as? [String: Any]
|
||
|
||
isLoading = false
|
||
} catch {
|
||
errorText = "NTS data unavailable"
|
||
isLoading = false
|
||
}
|
||
}
|
||
}
|
||
|
||
private func serverCountForRegion(_ region: String) -> Int {
|
||
return serverList.filter { ($0["region"] as? String) == region }.count
|
||
}
|
||
}
|