Initial commit: NetworkCity — network traffic as a neon city
A native macOS network monitor that renders live traffic as a top-down neon city: your Mac is downtown, remote orgs are glowing districts, and traffic is cars of light driving the roads. Architecture: - NetworkCityCore: swappable data layer behind a ConnectionSource protocol (nettop-backed today), plus pure/tested logic — traffic diffing, org classification (reverse-DNS + CIDR), traffic classes, hub-and-spoke expansion policy, label anti-overlap, and latency→distance layout. - NetworkCityApp: SwiftUI + SpriteKit city — auto-expanding districts, protocol-coloured cars, click-to-inspect panel, and latency-as-distance (districts glide to their measured RTT). - nettop-probe: CLI proof of the data layer. 44 tests passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import SwiftUI
|
||||
import SpriteKit
|
||||
import NetworkCityCore
|
||||
|
||||
/// The SpriteKit city with a glassy stats HUD floated on top.
|
||||
struct CityView: View {
|
||||
@ObservedObject var controller: TrafficController
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
SpriteView(scene: controller.scene, options: [.ignoresSiblingOrder])
|
||||
.ignoresSafeArea()
|
||||
|
||||
hud
|
||||
.padding(14)
|
||||
|
||||
if let inspection = controller.selection {
|
||||
inspector(inspection)
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.animation(.easeOut(duration: 0.18), value: controller.selection?.id)
|
||||
.onAppear { controller.start() }
|
||||
}
|
||||
|
||||
// MARK: - Inspector panel
|
||||
|
||||
static func color(_ c: TrafficClass) -> Color {
|
||||
switch c {
|
||||
case .dns: return Color(red: 1.00, green: 0.82, blue: 0.25)
|
||||
case .https: return Color(red: 0.22, green: 0.88, blue: 1.00)
|
||||
case .http: return Color(red: 1.00, green: 0.48, blue: 0.24)
|
||||
case .quic: return Color(red: 0.71, green: 0.48, blue: 1.00)
|
||||
case .other: return Color(red: 0.55, green: 0.60, blue: 0.72)
|
||||
}
|
||||
}
|
||||
|
||||
private func inspector(_ insp: OrgInspection) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text(insp.title)
|
||||
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.white)
|
||||
Spacer(minLength: 12)
|
||||
Button { controller.deselect() } label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(.white.opacity(0.5))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Text("▼ \(fmt(insp.downKBs))").foregroundStyle(.cyan)
|
||||
Text("▲ \(fmt(insp.upKBs))").foregroundStyle(.orange)
|
||||
Spacer(minLength: 4)
|
||||
Text("\(insp.endpointCount) pts").foregroundStyle(.white.opacity(0.6))
|
||||
}
|
||||
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||
|
||||
if !insp.processes.isEmpty {
|
||||
Text(insp.processes.joined(separator: ", "))
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.55))
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Divider().overlay(.white.opacity(0.15))
|
||||
|
||||
Text("ENDPOINTS").font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.4))
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
ForEach(insp.endpoints) { ep in endpointRow(ep) }
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 320)
|
||||
}
|
||||
.padding(14)
|
||||
.frame(width: 290)
|
||||
.background(.black.opacity(0.62), in: RoundedRectangle(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.white.opacity(0.1)))
|
||||
}
|
||||
|
||||
private func endpointRow(_ ep: EndpointInfo) -> some View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Circle().fill(Self.color(ep.topClass)).frame(width: 7, height: 7)
|
||||
.shadow(color: Self.color(ep.topClass), radius: 3)
|
||||
.padding(.top, 3)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(ep.host)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
if let rdns = ep.rdns {
|
||||
Text(rdns)
|
||||
.font(.system(size: 9, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.45))
|
||||
.lineLimit(1).truncationMode(.middle)
|
||||
}
|
||||
if let rtt = ep.rttMs {
|
||||
Text("\(Int(rtt)) ms")
|
||||
.font(.system(size: 9, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.5))
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 6)
|
||||
VStack(alignment: .trailing, spacing: 1) {
|
||||
if ep.downKBs >= 0.1 { Text("▼\(fmt(ep.downKBs))").foregroundStyle(.cyan.opacity(0.85)) }
|
||||
if ep.upKBs >= 0.1 { Text("▲\(fmt(ep.upKBs))").foregroundStyle(.orange.opacity(0.85)) }
|
||||
if ep.downKBs < 0.1 && ep.upKBs < 0.1 { Text("idle").foregroundStyle(.white.opacity(0.3)) }
|
||||
}
|
||||
.font(.system(size: 9, weight: .semibold, design: .monospaced))
|
||||
}
|
||||
}
|
||||
|
||||
private var hud: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(controller.live ? Color.green : Color.orange)
|
||||
.frame(width: 8, height: 8)
|
||||
Text("NETWORKCITY")
|
||||
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
}
|
||||
Divider().frame(width: 168).overlay(.white.opacity(0.15))
|
||||
stat("▼ down", controller.downKBs, .cyan)
|
||||
stat("▲ up", controller.upKBs, .orange)
|
||||
stat(label: "districts", value: "\(controller.districtCount)")
|
||||
|
||||
Divider().frame(width: 168).overlay(.white.opacity(0.15))
|
||||
legend
|
||||
|
||||
Text(controller.live ? "live · color = protocol · motion = direction"
|
||||
: "warming up…")
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.4))
|
||||
.padding(.top, 2)
|
||||
}
|
||||
.padding(12)
|
||||
.background(.black.opacity(0.45), in: RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.white.opacity(0.08)))
|
||||
}
|
||||
|
||||
private static let legendItems: [(String, Color)] = [
|
||||
("DNS", Color(red: 1.00, green: 0.82, blue: 0.25)),
|
||||
("HTTPS", Color(red: 0.22, green: 0.88, blue: 1.00)),
|
||||
("HTTP", Color(red: 1.00, green: 0.48, blue: 0.24)),
|
||||
("QUIC", Color(red: 0.71, green: 0.48, blue: 1.00)),
|
||||
("Other", Color(red: 0.55, green: 0.60, blue: 0.72)),
|
||||
]
|
||||
|
||||
private var legend: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(Self.legendItems, id: \.0) { name, color in
|
||||
HStack(spacing: 8) {
|
||||
Circle().fill(color).frame(width: 7, height: 7)
|
||||
.shadow(color: color, radius: 3)
|
||||
Text(name)
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 168, alignment: .leading)
|
||||
}
|
||||
|
||||
private func stat(_ label: String, _ kbs: Double, _ color: Color) -> some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundStyle(color.opacity(0.9))
|
||||
Spacer(minLength: 16)
|
||||
Text(format(kbs))
|
||||
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.frame(width: 168)
|
||||
}
|
||||
|
||||
private func stat(label: String, value: String) -> some View {
|
||||
HStack {
|
||||
Text(label).font(.system(size: 12, design: .monospaced)).foregroundStyle(.white.opacity(0.7))
|
||||
Spacer(minLength: 16)
|
||||
Text(value).font(.system(size: 12, weight: .semibold, design: .monospaced)).foregroundStyle(.white)
|
||||
}
|
||||
.frame(width: 168)
|
||||
}
|
||||
|
||||
private func format(_ kbs: Double) -> String {
|
||||
kbs >= 1024 ? String(format: "%.1f MB/s", kbs / 1024) : String(format: "%.0f KB/s", kbs)
|
||||
}
|
||||
|
||||
/// Compact rate for the dense inspector rows.
|
||||
private func fmt(_ kbs: Double) -> String {
|
||||
kbs >= 1024 ? String(format: "%.1fM", kbs / 1024) : String(format: "%.0fK", kbs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user