181 lines
6.2 KiB
Swift
181 lines
6.2 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
|
|
enum CodexAccountHealth: Equatable {
|
|
case ok
|
|
case needsReauth
|
|
case workspaceDeactivated
|
|
case missingAuth
|
|
case unavailable
|
|
|
|
var label: String? {
|
|
switch self {
|
|
case .ok:
|
|
nil
|
|
case .needsReauth:
|
|
"Needs re-auth"
|
|
case .workspaceDeactivated:
|
|
"Workspace deactivated"
|
|
case .missingAuth:
|
|
"Missing auth"
|
|
case .unavailable:
|
|
"Unavailable"
|
|
}
|
|
}
|
|
|
|
static func status(for account: CodexVisibleAccount, error: String?) -> CodexAccountHealth {
|
|
if let error {
|
|
return self.status(forError: error)
|
|
}
|
|
if account.authenticationHealthLabel != nil {
|
|
return .missingAuth
|
|
}
|
|
return .ok
|
|
}
|
|
|
|
static func status(forError error: String) -> CodexAccountHealth {
|
|
let normalized = error.lowercased()
|
|
if normalized.contains("deactivated") {
|
|
return .workspaceDeactivated
|
|
}
|
|
if normalized.contains("expired") ||
|
|
normalized.contains("revoked") ||
|
|
normalized.contains("unauthorized") ||
|
|
normalized.contains("401")
|
|
{
|
|
return .needsReauth
|
|
}
|
|
if normalized.contains("missing"), normalized.contains("auth") {
|
|
return .missingAuth
|
|
}
|
|
return .unavailable
|
|
}
|
|
}
|
|
|
|
enum CodexAccountPresentationOrdering {
|
|
static func orderedAccounts(
|
|
_ accounts: [CodexVisibleAccount],
|
|
snapshots: [CodexAccountUsageSnapshot],
|
|
activeVisibleAccountID: String?)
|
|
-> [CodexVisibleAccount]
|
|
{
|
|
guard accounts.count > 1 else { return accounts }
|
|
let snapshotByID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) })
|
|
let rankedAccounts = accounts.enumerated().map { index, account in
|
|
RankedAccount(
|
|
account: account,
|
|
rank: Rank(
|
|
account: account,
|
|
snapshot: snapshotByID[account.id],
|
|
activeVisibleAccountID: activeVisibleAccountID,
|
|
originalIndex: index))
|
|
}
|
|
let grouped = Dictionary(grouping: rankedAccounts, by: { Self.workspaceSortKey(for: $0.account) })
|
|
return grouped.values.sorted { lhs, rhs in
|
|
(lhs.map(\.rank).min() ?? .last) < (rhs.map(\.rank).min() ?? .last)
|
|
}.flatMap { group in
|
|
group.sorted { lhs, rhs in lhs.rank < rhs.rank }.map(\.account)
|
|
}
|
|
}
|
|
|
|
private struct RankedAccount {
|
|
let account: CodexVisibleAccount
|
|
let rank: Rank
|
|
}
|
|
|
|
private struct Rank: Comparable {
|
|
static let last = Rank(bucket: Int.max, availabilityScore: -.greatestFiniteMagnitude, originalIndex: Int.max)
|
|
|
|
let bucket: Int
|
|
let availabilityScore: Double
|
|
let displaySort: String
|
|
let originalIndex: Int
|
|
|
|
private init(bucket: Int, availabilityScore: Double, originalIndex: Int) {
|
|
self.bucket = bucket
|
|
self.availabilityScore = availabilityScore
|
|
self.displaySort = ""
|
|
self.originalIndex = originalIndex
|
|
}
|
|
|
|
init(
|
|
account: CodexVisibleAccount,
|
|
snapshot: CodexAccountUsageSnapshot?,
|
|
activeVisibleAccountID: String?,
|
|
originalIndex: Int)
|
|
{
|
|
self.originalIndex = originalIndex
|
|
self.displaySort = account.menuDisplayName.lowercased()
|
|
|
|
if account.id == activeVisibleAccountID {
|
|
self.bucket = 0
|
|
} else {
|
|
let health = CodexAccountHealth.status(for: account, error: snapshot?.error)
|
|
if health != .ok {
|
|
self.bucket = health == .missingAuth ? 4 : 3
|
|
} else if let availability = Self.availability(snapshot?.snapshot), availability <= 0 {
|
|
self.bucket = 2
|
|
} else {
|
|
self.bucket = 1
|
|
}
|
|
}
|
|
self.availabilityScore = Self.availability(snapshot?.snapshot) ?? -1
|
|
}
|
|
|
|
static func < (lhs: Rank, rhs: Rank) -> Bool {
|
|
if lhs.bucket != rhs.bucket { return lhs.bucket < rhs.bucket }
|
|
if lhs.availabilityScore != rhs.availabilityScore {
|
|
return lhs.availabilityScore > rhs.availabilityScore
|
|
}
|
|
if lhs.displaySort != rhs.displaySort { return lhs.displaySort < rhs.displaySort }
|
|
return lhs.originalIndex < rhs.originalIndex
|
|
}
|
|
|
|
private static func availability(_ snapshot: UsageSnapshot?) -> Double? {
|
|
guard let snapshot else { return nil }
|
|
let session = snapshot.primary?.remainingPercent
|
|
let weekly = snapshot.secondary?.remainingPercent
|
|
return switch (session, weekly) {
|
|
case let (.some(session), .some(weekly)):
|
|
min(session, weekly)
|
|
case let (.some(session), .none):
|
|
session
|
|
case let (.none, .some(weekly)):
|
|
weekly
|
|
case (.none, .none):
|
|
nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func workspaceSortKey(for account: CodexVisibleAccount) -> String {
|
|
if let workspaceAccountID = account.workspaceAccountID, !workspaceAccountID.isEmpty {
|
|
return workspaceAccountID.lowercased()
|
|
}
|
|
return account.menuWorkspaceLabel?.lowercased() ?? "personal"
|
|
}
|
|
}
|
|
|
|
struct CodexAccountWorkspaceSection: Equatable {
|
|
let title: String
|
|
let accounts: [CodexVisibleAccount]
|
|
}
|
|
|
|
extension [CodexVisibleAccount] {
|
|
func codexWorkspaceSections() -> [CodexAccountWorkspaceSection] {
|
|
guard !self.isEmpty else { return [] }
|
|
var sections: [CodexAccountWorkspaceSection] = []
|
|
for account in self {
|
|
let title = account.menuWorkspaceLabel ?? "Personal"
|
|
if let index = sections.firstIndex(where: { $0.title == title }) {
|
|
var accounts = sections[index].accounts
|
|
accounts.append(account)
|
|
sections[index] = CodexAccountWorkspaceSection(title: title, accounts: accounts)
|
|
} else {
|
|
sections.append(CodexAccountWorkspaceSection(title: title, accounts: [account]))
|
|
}
|
|
}
|
|
return sections
|
|
}
|
|
}
|