← back to Exo
Place local node at the top of the dashboard. (#1033)
fea42473ddc54c29691524b621618bd8977e721c · 2025-12-28 21:12:47 +0000 · Evan Quiney
@samiamjidkhan and @AlexCheema's work moving the topology to place the
local node at the top of the topology in the app dashboard.
Files touched
M app/EXO/EXO/ContentView.swiftM app/EXO/EXO/Services/BugReportService.swiftM app/EXO/EXO/Services/ClusterStateService.swiftM app/EXO/EXO/ViewModels/NodeViewModel.swift
Diff
commit fea42473ddc54c29691524b621618bd8977e721c
Author: Evan Quiney <evanev7@gmail.com>
Date: Sun Dec 28 21:12:47 2025 +0000
Place local node at the top of the dashboard. (#1033)
@samiamjidkhan and @AlexCheema's work moving the topology to place the
local node at the top of the topology in the app dashboard.
---
app/EXO/EXO/ContentView.swift | 2 +-
app/EXO/EXO/Services/BugReportService.swift | 1 -
app/EXO/EXO/Services/ClusterStateService.swift | 24 ++++++++++++++++++++++++
app/EXO/EXO/ViewModels/NodeViewModel.swift | 12 +++++++-----
4 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift
index 7e6372e0..2b8ba2bd 100644
--- a/app/EXO/EXO/ContentView.swift
+++ b/app/EXO/EXO/ContentView.swift
@@ -49,7 +49,7 @@ struct ContentView: View {
private var topologySection: some View {
Group {
- if let topology = stateService.latestSnapshot?.topologyViewModel(), !topology.nodes.isEmpty {
+ if let topology = stateService.latestSnapshot?.topologyViewModel(localNodeId: stateService.localNodeId), !topology.nodes.isEmpty {
TopologyMiniView(topology: topology)
}
}
diff --git a/app/EXO/EXO/Services/BugReportService.swift b/app/EXO/EXO/Services/BugReportService.swift
index a1852c6b..b578deb9 100644
--- a/app/EXO/EXO/Services/BugReportService.swift
+++ b/app/EXO/EXO/Services/BugReportService.swift
@@ -82,7 +82,6 @@ struct BugReportService {
}
private func loadCredentials() throws -> AWSConfig {
- // These credentials are write-only and necessary to receive bug reports from users
return AWSConfig(
accessKey: "AKIAYEKP5EMXTOBYDGHX",
secretKey: "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",
diff --git a/app/EXO/EXO/Services/ClusterStateService.swift b/app/EXO/EXO/Services/ClusterStateService.swift
index dddbff4b..65097e8c 100644
--- a/app/EXO/EXO/Services/ClusterStateService.swift
+++ b/app/EXO/EXO/Services/ClusterStateService.swift
@@ -7,6 +7,7 @@ final class ClusterStateService: ObservableObject {
@Published private(set) var lastError: String?
@Published private(set) var lastActionMessage: String?
@Published private(set) var modelOptions: [ModelOption] = []
+ @Published private(set) var localNodeId: String?
private var timer: Timer?
private let decoder: JSONDecoder
@@ -29,6 +30,7 @@ final class ClusterStateService: ObservableObject {
func startPolling(interval: TimeInterval = 0.5) {
stopPolling()
Task {
+ await fetchLocalNodeId()
await fetchModels()
await fetchSnapshot()
}
@@ -46,9 +48,31 @@ final class ClusterStateService: ObservableObject {
latestSnapshot = nil
lastError = nil
lastActionMessage = nil
+ localNodeId = nil
+ }
+
+ private func fetchLocalNodeId() async {
+ do {
+ let url = baseURL.appendingPathComponent("node_id")
+ var request = URLRequest(url: url)
+ request.cachePolicy = .reloadIgnoringLocalCacheData
+ let (data, response) = try await session.data(for: request)
+ guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else {
+ return
+ }
+ if let nodeId = try? decoder.decode(String.self, from: data) {
+ localNodeId = nodeId
+ }
+ } catch {
+ // Silently ignore - localNodeId will remain nil and retry on next poll
+ }
}
private func fetchSnapshot() async {
+ // Retry fetching local node ID if not yet set
+ if localNodeId == nil {
+ await fetchLocalNodeId()
+ }
do {
var request = URLRequest(url: endpoint)
request.cachePolicy = .reloadIgnoringLocalCacheData
diff --git a/app/EXO/EXO/ViewModels/NodeViewModel.swift b/app/EXO/EXO/ViewModels/NodeViewModel.swift
index 684f4487..3c72155d 100644
--- a/app/EXO/EXO/ViewModels/NodeViewModel.swift
+++ b/app/EXO/EXO/ViewModels/NodeViewModel.swift
@@ -85,7 +85,7 @@ struct TopologyViewModel {
}
extension ClusterState {
- func topologyViewModel() -> TopologyViewModel? {
+ func topologyViewModel(localNodeId: String?) -> TopologyViewModel? {
let topologyNodeIds = Set(topology?.nodes.map(\.nodeId) ?? [])
let allNodes = nodeViewModels().filter { topologyNodeIds.isEmpty || topologyNodeIds.contains($0.id) }
guard !allNodes.isEmpty else { return nil }
@@ -105,6 +105,11 @@ extension ClusterState {
orderedNodes = allNodes
}
+ // Rotate so the local node (from /node_id API) is first
+ if let localId = localNodeId, let index = orderedNodes.firstIndex(where: { $0.id == localId }) {
+ orderedNodes = Array(orderedNodes[index...]) + Array(orderedNodes[..<index])
+ }
+
let nodeIds = Set(orderedNodes.map(\.id))
let edgesArray: [TopologyEdgeViewModel] = topology?.connections?.compactMap { connection in
guard nodeIds.contains(connection.localNodeId), nodeIds.contains(connection.sendBackNodeId) else { return nil }
@@ -112,10 +117,7 @@ extension ClusterState {
} ?? []
let edges = Set(edgesArray)
- let topologyRootId = topology?.nodes.first?.nodeId
- let currentId = orderedNodes.first(where: { $0.id == topologyRootId })?.id ?? orderedNodes.first?.id
-
- return TopologyViewModel(nodes: orderedNodes, edges: Array(edges), currentNodeId: currentId)
+ return TopologyViewModel(nodes: orderedNodes, edges: Array(edges), currentNodeId: localNodeId)
}
}
← ca7adcc2 Update README.md with instructions to enable RDMA. (#1031)
·
back to Exo
·
fix warmup order. should be rank!=0 then rank=0 ade3ee7e →