@@ -15,10 +15,6 @@ final class AppState {
1515 private let networkMonitor = NWPathMonitor()
1616 private let networkMonitorQueue = DispatchQueue(label: "com.parseableviewer.network-monitor")
1717 private var connectTask: Task<Bool, Never>?
18− /// The connection `connectTask` is connecting to. `activeConnection` only
19− /// switches once the new server answers, so this is what identifies an
20− /// in-flight connect whose target the user deletes (see `removeConnection`).
21− private var connectingConnectionID: UUID?
2218
2319 // MARK: - Stream State
2420 var streams: [LogStream] = []
@@ -100,7 +96,6 @@ final class AppState {
10096
10197 if let activeID = ConnectionStore.loadActiveConnectionID(),
10298 let connection = connections.first(where: { $0.id == activeID }) {
103− connectingConnectionID = connection.id
10499 connectTask = Task { @MainActor [weak self] in
105100 await self?.performConnect(to: connection) ?? false
106101 }
@@ -120,7 +115,6 @@ final class AppState {
120115 existing.cancel()
121116 _ = await existing.value
122117 }
123− connectingConnectionID = connection.id
124118 let task = Task { @MainActor [weak self] in
125119 await self?.performConnect(to: connection) ?? false
126120 }
@@ -130,16 +124,9 @@ final class AppState {
130124
131125 @MainActor
132126 private func performConnect(to connection: ServerConnection) async -> Bool {
133− defer {
134− if connectingConnectionID == connection.id { connectingConnectionID = nil }
135− }
136127 guard !isConnecting else { return false }
137128
138− // NWPathMonitor ignores loopback, so with no active network interface it
139− // reports "unsatisfied" even though a local server (the common docker /
140− // dev setup) is reachable. Don't block those; a genuinely unreachable
141− // host still fails below with the same offline message.
142− if !isNetworkAvailable && !Self.isLoopback(connection.baseURL) {
129+ if !isNetworkAvailable {
143130 self.errorMessage = "No internet connection. Check your network and try again."
144131 self.showError = true
145132 return false
@@ -147,12 +134,7 @@ final class AppState {
147134
148135 isConnecting = true
149136 defer { isConnecting = false }
150− // Reset the flag with the message: a previous failed connect left
151− // `showError` set, and if its alert couldn't present (e.g. behind the
152− // connection sheet), it would pop up later — after a successful retry —
153− // reading "An unknown error occurred".
154137 errorMessage = nil
155− showError = false
156138
157139 // When switching to a different server, clear stream-specific state
158140 // so stale selections don't reference streams that don't exist.
@@ -182,7 +164,6 @@ final class AppState {
182164
183165 // Test connection — throws on non-200 or network failure
184166 try await newClient.checkHealth()
185− try await Self.verifyCredentials(with: newClient)
186167
187168 // If the task was cancelled while awaiting (e.g. user clicked
188169 // Disconnect during auto-reconnect), bail before installing the
@@ -219,12 +200,6 @@ final class AppState {
219200 case .success(let list):
220201 self.streams = list
221202 self.streamLoadError = nil
222− // Like refreshStreams: a selection that doesn't exist on this server
223− // (e.g. one picked from a stale list while the connect was pending)
224− // would keep every stream tab querying a missing stream.
225− if let selected = selectedStream, !list.contains(where: { $0.name == selected }) {
226− selectedStream = nil
227− }
228203 case .failure(let error):
229204 self.streams = []
230205 self.streamLoadError = ParseableError.userFriendlyMessage(for: error)
@@ -290,13 +265,8 @@ final class AppState {
290265 let loaded = try await client.listStreams()
291266 // Drop results if the active connection changed while awaiting. Still
292267 // clear isLoadingStreams: this task set the spinner, so leaving it true
293− // wedges the indicator and disables Refresh. Compare the client too:
294− // editing the active connection's URL/credentials keeps its id but
295− // installs a new client for a possibly different server.
296− guard connID == activeConnection?.id, client === self.client else {
297− isLoadingStreams = false
298− return
299− }
268+ // wedges the indicator and disables Refresh.
269+ guard connID == activeConnection?.id else { isLoadingStreams = false; return }
300270 streams = loaded
301271 // Drop a selection that no longer exists server-side (e.g. the stream
302272 // was deleted from another client); otherwise the detail tabs keep
@@ -305,10 +275,7 @@ final class AppState {
305275 selectedStream = nil
306276 }
307277 } catch {
308− guard connID == activeConnection?.id, client === self.client else {
309− isLoadingStreams = false
310− return
311− }
278+ guard connID == activeConnection?.id else { isLoadingStreams = false; return }
312279 let message = ParseableError.userFriendlyMessage(for: error)
313280 streamLoadError = message
314281 self.errorMessage = "Failed to load streams: \(message)"
@@ -375,49 +342,9 @@ final class AppState {
375342 ConnectionStore.saveConnections(connections)
376343 if activeConnection?.id == connection.id {
377344 disconnect()
378− } else if connectingConnectionID == connection.id {
379− // The removed connection is still being connected to (e.g. Cancel in the
380− // connection sheet, or deleting it in Settings during the launch
381− // reconnect). `activeConnection` doesn't point at it yet, so without
382− // this the connect would carry on and install a client for a
383− // connection that no longer exists (or tear down the current session
384− // when it fails) after the user abandoned it.
385− connectTask?.cancel()
386− connectTask = nil
387− connectingConnectionID = nil
388− // Starting that connect already cleared the current server's stream
389− // list and filters; reload them if we're still connected to it.
390− if client != nil {
391− Task { @MainActor [weak self] in
392− await self?.refreshStreams()
393− await self?.refreshFilters()
394− }
395− }
396− }
397− }
398−
399− /// `/api/v1/liveness` is unauthenticated on Parseable (it answers 200 to
400− /// anyone, for load-balancer / k8s probes), so a health check alone accepts
401− /// any username and password. Probe an authenticated endpoint and fail on
402− /// 401. Other failures — e.g. a 403 for a role without the about permission —
403− /// don't show the credentials are wrong, so they're ignored here. The client
404− /// caches the response, so the follow-up `getAbout()` load reuses it.
405− static func verifyCredentials(with client: ParseableClient) async throws {
406− do {
407− _ = try await client.getAbout()
408− } catch ParseableError.unauthorized {
409− throw ParseableError.unauthorized
410− } catch {
411− // Not an authentication failure; leave it to the best-effort loads.
412345 }
413346 }
414347
415− private static func isLoopback(_ url: URL?) -> Bool {
416− guard let host = url?.host?.lowercased() else { return false }
417− return host == "localhost" || host.hasSuffix(".localhost")
418− || host == "::1" || host.hasPrefix("127.")
419− }
420−
421348 // MARK: - Stream Management
422349
423350 @MainActor
@@ -450,10 +377,7 @@ final class AppState {
450377 let connID = activeConnection?.id
451378 isLoadingFilters = true
452379 let loaded = (try? await client.listFilters()) ?? []
453− guard connID == activeConnection?.id, client === self.client else {
454− isLoadingFilters = false
455− return
456− }
380+ guard connID == activeConnection?.id else { isLoadingFilters = false; return }
457381 filters = loaded
458382 isLoadingFilters = false
459383 }