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
|
|||
|
|
}
|
|||
|
|
}
|