Compare commits

...

3 Commits

Author SHA1 Message Date
timban 34cdcee00b Group Microsoft/OneDrive no-PTR ranges into one district
OneDrive's IPs (e.g. 13.107.x, 150.171.x) have no PTR and weren't in our
CIDR table, so each fragmented into its own IP-named district — a long
session showed dozens, all with the "OneDrive" process subtitle. Add the
documented Microsoft 365 service ranges so they collapse to one Microsoft
district. Deliberately excludes generic Azure tenant space, which would
mislabel third-party apps hosted on Azure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:46:53 -07:00
timban 02f48ddd56 Add idle district lifecycle: decay then reap
The map only ever gained density: hubs were sized by lifetime-cumulative
bytes (so they never shrank) and were never removed. Now each hub has a
decaying activity level — quiet districts visibly shrink toward a dormant
dot — and after a configurable idle timeout (~40s, NC_REAP_TICKS) the hub
is demolished: faded out, removed with its road, and its per-org state
pruned in the controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:46:53 -07:00
timban 19f8cf08d7 Fix label overlap when districts glide to their latency distance
The latency reposition moved hubs to their RTT radius without checking
label collisions, so a gliding district could land on top of a neighbour.
Make both placement paths overlap-aware via the existing LayoutSolver:
keep distance = latency (the meaningful axis) but rotate the arbitrary
angle to clear other labels. Track each hub's target position so
same-tick moves avoid where others are heading, not just where they are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:46:53 -07:00
5 changed files with 103 additions and 10 deletions
+22 -7
View File
@@ -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.
+49 -3
View File
@@ -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] = []
+10
View File
@@ -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")