Compare commits
3 Commits
c08fc277a9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 34cdcee00b | |||
| 02f48ddd56 | |||
| 19f8cf08d7 |
@@ -39,7 +39,7 @@ final class BuildingNode: SKNode {
|
||||
private let core: SKShapeNode
|
||||
private let title: SKLabelNode
|
||||
private let subtitle: SKLabelNode
|
||||
private var cumulative: Double = 0
|
||||
private var activity: Double = 0
|
||||
|
||||
private let spokeLayer = SKNode()
|
||||
private let endpointLayer = SKNode()
|
||||
@@ -106,13 +106,11 @@ final class BuildingNode: SKNode {
|
||||
|
||||
func note(activeHosts: [String]) { knownHosts.formUnion(activeHosts) }
|
||||
|
||||
/// React to a tick of traffic: grow toward a size set by total volume, and
|
||||
/// pulse so the eye catches the activity.
|
||||
/// React to a tick of traffic: bump the (decaying) activity level, grow
|
||||
/// toward a size set by it, and pulse so the eye catches the activity.
|
||||
func receive(bytes: Double) {
|
||||
cumulative += bytes
|
||||
let target = (0.30 + CGFloat(log10(cumulative + 1)) * 0.09).clamped(0.30, 1.05)
|
||||
glow.removeAction(forKey: "resize")
|
||||
glow.run(.scale(to: target, duration: 0.6), withKey: "resize")
|
||||
activity += bytes
|
||||
updateGlowSize()
|
||||
|
||||
core.removeAction(forKey: "pulse")
|
||||
core.run(.sequence([
|
||||
@@ -121,6 +119,23 @@ final class BuildingNode: SKNode {
|
||||
]), withKey: "pulse")
|
||||
}
|
||||
|
||||
/// Called every tick. Activity decays so quiet hubs visibly shrink toward
|
||||
/// dormant instead of staying bright forever.
|
||||
func decay(factor: Double = 0.55) {
|
||||
guard activity > 0 else { return }
|
||||
activity *= factor
|
||||
updateGlowSize()
|
||||
}
|
||||
|
||||
/// Ticks since this hub last carried traffic — used to reap dead districts.
|
||||
var idleTicks = 0
|
||||
|
||||
private func updateGlowSize() {
|
||||
let target = (0.20 + CGFloat(log10(activity + 1)) * 0.085).clamped(0.20, 1.05)
|
||||
glow.removeAction(forKey: "resize")
|
||||
glow.run(.scale(to: target, duration: 0.6), withKey: "resize")
|
||||
}
|
||||
|
||||
// MARK: - Expansion
|
||||
|
||||
/// The capped, stably-ordered list of endpoints we actually draw.
|
||||
|
||||
@@ -38,6 +38,7 @@ final class CityScene: SKScene {
|
||||
private var buildings: [String: BuildingNode] = [:]
|
||||
private var roads: [String: SKShapeNode] = [:]
|
||||
private var latencyRadius: [String: CGFloat] = [:]
|
||||
private var targetPos: [String: CGPoint] = [:] // where each hub is heading
|
||||
private var ringKey: String?
|
||||
private let roadLayer = SKNode()
|
||||
private let carLayer = SKNode()
|
||||
@@ -53,6 +54,10 @@ final class CityScene: SKScene {
|
||||
/// Called with a district key when a hub is clicked, or nil when empty space
|
||||
/// is clicked (deselect).
|
||||
var onSelect: ((String?) -> Void)?
|
||||
/// Called when a district is demolished, so the controller can prune its state.
|
||||
var onReap: ((String) -> Void)?
|
||||
/// Demolish a district after this many idle ticks (~2s each). Env-tunable for testing.
|
||||
private let reapAfterTicks = Int(ProcessInfo.processInfo.environment["NC_REAP_TICKS"] ?? "") ?? 20
|
||||
private let selectionRing = SKShapeNode(circleOfRadius: 24)
|
||||
private var dragDistance: CGFloat = 0
|
||||
|
||||
@@ -159,9 +164,17 @@ final class CityScene: SKScene {
|
||||
/// Render a tick of per-org traffic: ensure each district exists, decide
|
||||
/// which ones are busy enough to fan out, then dispatch cars accordingly.
|
||||
func apply(_ districts: [DistrictTraffic]) {
|
||||
// 0. Age every hub: decay its glow and count idle ticks. Active hubs
|
||||
// reset below; dead ones get reaped at the end of the tick.
|
||||
for (_, hub) in buildings {
|
||||
hub.decay()
|
||||
hub.idleTicks += 1
|
||||
}
|
||||
|
||||
// 1. Ensure hubs exist and learn their endpoints.
|
||||
for t in districts {
|
||||
let hub = building(forKey: t.key, title: t.title, subtitle: t.subtitle)
|
||||
hub.idleTicks = 0
|
||||
hub.note(activeHosts: t.endpoints.map(\.host))
|
||||
hub.receive(bytes: Double(t.inBytes + t.outBytes))
|
||||
if let rtt = t.rttMs { repositionToLatency(key: t.key, rttMs: rtt) }
|
||||
@@ -217,6 +230,26 @@ final class CityScene: SKScene {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Demolish districts that have been idle too long.
|
||||
for key in buildings.keys where (buildings[key]?.idleTicks ?? 0) > reapAfterTicks {
|
||||
reap(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fade out and remove a dead district, clearing all of its scene state.
|
||||
private func reap(_ key: String) {
|
||||
buildings[key]?.run(.sequence([.fadeOut(withDuration: 0.7), .removeFromParent()]))
|
||||
roads[key]?.run(.sequence([.fadeOut(withDuration: 0.7), .removeFromParent()]))
|
||||
buildings[key] = nil
|
||||
roads[key] = nil
|
||||
latencyRadius[key] = nil
|
||||
targetPos[key] = nil
|
||||
if ringKey == key { highlight(key: nil); onSelect?(nil) }
|
||||
onReap?(key)
|
||||
if ProcessInfo.processInfo.environment["NC_DEBUG"] != nil {
|
||||
FileHandle.standardError.write(Data("[reap] \(key)\n".utf8))
|
||||
}
|
||||
}
|
||||
|
||||
private func carCount(_ bytes: UInt64) -> Int {
|
||||
@@ -234,6 +267,7 @@ final class CityScene: SKScene {
|
||||
|
||||
let node = BuildingNode(title: title, subtitle: subtitle)
|
||||
node.position = placement(for: key, footprint: node.labelFootprint)
|
||||
targetPos[key] = node.position
|
||||
node.alpha = 0
|
||||
node.run(.fadeIn(withDuration: 0.6))
|
||||
buildingLayer.addChild(node)
|
||||
@@ -266,8 +300,19 @@ final class CityScene: SKScene {
|
||||
if let current = latencyRadius[key], abs(current - target) < 18 { return }
|
||||
latencyRadius[key] = target
|
||||
|
||||
// Keep distance = latency, but rotate/nudge the (arbitrary) angle to
|
||||
// clear other labels — otherwise gliding hubs land on top of each other.
|
||||
let angle = atan2(building.position.y, building.position.x)
|
||||
let dest = CGPoint(x: cos(angle) * Double(target), y: sin(angle) * Double(target))
|
||||
let others = buildings.compactMap { (k, b) -> CGRect? in
|
||||
guard k != key else { return nil }
|
||||
let p = targetPos[k] ?? b.position
|
||||
return b.labelFootprint.offsetBy(dx: p.x, dy: p.y)
|
||||
}
|
||||
let dest = LayoutSolver.placeNonOverlapping(
|
||||
baseAngle: angle, baseRadius: Double(target),
|
||||
localRect: building.labelFootprint, existing: others, pad: 18
|
||||
)
|
||||
targetPos[key] = dest
|
||||
building.run(.move(to: dest, duration: 0.8))
|
||||
|
||||
if let road = roads[key] {
|
||||
@@ -287,8 +332,9 @@ final class CityScene: SKScene {
|
||||
let ring = Double((h >> 16) % 1000) / 1000
|
||||
let baseRadius = 165 + Double(ring) * Double(worldRadius - 165)
|
||||
|
||||
let existing = buildings.values.map {
|
||||
$0.labelFootprint.offsetBy(dx: $0.position.x, dy: $0.position.y)
|
||||
let existing = buildings.map { (k, b) in
|
||||
let p = targetPos[k] ?? b.position
|
||||
return b.labelFootprint.offsetBy(dx: p.x, dy: p.y)
|
||||
}
|
||||
return LayoutSolver.placeNonOverlapping(
|
||||
baseAngle: baseAngle, baseRadius: baseRadius,
|
||||
|
||||
@@ -39,6 +39,9 @@ final class TrafficController: ObservableObject {
|
||||
scene.onSelect = { [weak self] key in
|
||||
MainActor.assumeIsolated { self?.select(key) }
|
||||
}
|
||||
scene.onReap = { [weak self] key in
|
||||
MainActor.assumeIsolated { self?.prune(key) }
|
||||
}
|
||||
if ProcessInfo.processInfo.environment["NC_DEMO"] != nil {
|
||||
startDemo()
|
||||
return
|
||||
@@ -175,6 +178,17 @@ final class TrafficController: ObservableObject {
|
||||
|
||||
func deselect() { select(nil) }
|
||||
|
||||
/// A district was demolished — drop its rendering/inspection state. The
|
||||
/// resolution + RTT caches are kept (small, bounded by distinct hosts, and
|
||||
/// save rework if the district comes back).
|
||||
private func prune(_ key: String) {
|
||||
orgEndpoints[key] = nil
|
||||
orgProcesses[key] = nil
|
||||
orgTitles[key] = nil
|
||||
lastEndpointBytes[key] = nil
|
||||
if selectedKey == key { selectedKey = nil; selection = nil }
|
||||
}
|
||||
|
||||
private func buildInspection(_ key: String) -> OrgInspection {
|
||||
let hosts = orgEndpoints[key] ?? []
|
||||
var infos: [EndpointInfo] = []
|
||||
|
||||
@@ -13,6 +13,16 @@ public enum IPRanges {
|
||||
("149.154.160.0/20", "telegram", "Telegram"),
|
||||
("91.108.0.0/16", "telegram", "Telegram"),
|
||||
("13.64.0.0/11", "microsoft", "Microsoft"),
|
||||
// Microsoft 365 / OneDrive front-ends — no PTR, so without these each
|
||||
// IP fragments into its own district. These are Microsoft-operated
|
||||
// service ranges (not generic Azure tenant space, which would wrongly
|
||||
// relabel third-party apps hosted on Azure).
|
||||
("13.104.0.0/14", "microsoft", "Microsoft"), // incl. 13.107.x
|
||||
("150.171.0.0/16", "microsoft", "Microsoft"),
|
||||
("52.108.0.0/14", "microsoft", "Microsoft"), // O365 common
|
||||
("52.112.0.0/14", "microsoft", "Microsoft"), // Teams/Skype
|
||||
("40.96.0.0/13", "microsoft", "Microsoft"), // Exchange Online
|
||||
("40.104.0.0/15", "microsoft", "Microsoft"),
|
||||
("157.240.0.0/16", "meta", "Meta"),
|
||||
("31.13.24.0/21", "meta", "Meta"),
|
||||
("129.134.0.0/16", "meta", "Meta"),
|
||||
|
||||
@@ -12,6 +12,14 @@ final class IPRangesTests: XCTestCase {
|
||||
XCTAssertEqual(IPRanges.match("149.154.175.54")?.key, "telegram")
|
||||
}
|
||||
|
||||
func testOneDriveNoPTRRangesCollapseToMicrosoft() {
|
||||
// Real OneDrive front-end IPs (no PTR) that used to fragment per-IP.
|
||||
XCTAssertEqual(IPRanges.match("13.107.137.11")?.key, "microsoft")
|
||||
XCTAssertEqual(IPRanges.match("150.171.22.11")?.name, "Microsoft")
|
||||
XCTAssertEqual(IPRanges.match("52.108.1.1")?.key, "microsoft")
|
||||
XCTAssertEqual(IPRanges.match("40.96.0.1")?.key, "microsoft")
|
||||
}
|
||||
|
||||
func testPrivateRangesAreLocalNetwork() {
|
||||
XCTAssertEqual(IPRanges.match("192.168.0.1")?.name, "Local Network")
|
||||
XCTAssertEqual(IPRanges.match("10.1.2.3")?.key, "lan")
|
||||
|
||||
Reference in New Issue
Block a user