Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -741,16 +741,18 @@ struct AccountRowView: View {
.truncationMode(.middle)
.layoutPriority(1)

Text("·")
.font(PanelFont.accountDetail)
.foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.62))
.fixedSize(horizontal: true, vertical: false)
if let capacityLabel = account.currentCapacityLabel {
Text("·")
.font(PanelFont.accountDetail)
.foregroundStyle(PanelPalette.secondaryText(colorScheme).opacity(0.62))
.fixedSize(horizontal: true, vertical: false)

Text(account.capacityLabel)
.font(PanelFont.accountDetail)
.foregroundStyle(PanelPalette.secondaryText(colorScheme))
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
Text(capacityLabel)
.font(PanelFont.accountDetail)
.foregroundStyle(PanelPalette.secondaryText(colorScheme))
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
}

if let healthLabel = account.compactHealthLabel {
Text("·")
Expand All @@ -777,7 +779,7 @@ struct AccountRowView: View {
isPrimary: true,
size: 21,
action: login,
help: "Login account"
help: loginHelp
)
} else {
PanelIconButtonView(
Expand Down Expand Up @@ -850,14 +852,22 @@ struct AccountRowView: View {
return "Restore balanced run routing"
}
if account.needsLogin {
return "Login before routing runs"
return "Sign in again before routing runs"
}
if account.disabled {
return "Disabled account cannot route runs"
}

return "Route Decodex runs here"
}

private var loginHelp: String {
if account.recoveryActionKind == .login {
return "Refresh token was rejected; sign in again"
}

return "Login account"
}
}

struct AccountRunSummaryView: View {
Expand Down Expand Up @@ -2359,7 +2369,7 @@ private enum AccountPrivacy {
static let visibleValue = "visible"
}

private enum AccountDisplay {
enum AccountDisplay {
static let randomNames = [
"Alex",
"Avery",
Expand Down Expand Up @@ -2447,7 +2457,7 @@ private enum AccountDisplay {
return "\(local)\(domain)"
}

return "\(local.prefix(3))...\(local.suffix(3))\(domain)"
return "\(local.prefix(3))...\(compactLocalSuffix(local))\(domain)"
}

static func compactIdentity(_ value: String) -> String {
Expand All @@ -2469,6 +2479,17 @@ private enum AccountDisplay {
return text
}

private static func compactLocalSuffix(_ local: String) -> String {
if let separator = local.lastIndex(of: ".") {
let segment = String(local[local.index(after: separator)...])
if (2...4).contains(segment.count), segment.allSatisfy(\.isLetter) {
return segment
}
}

return String(local.suffix(3))
}

private static func identityHash(_ value: String) -> UInt32 {
var hash: UInt32 = 2_166_136_261
for unit in value.utf16 {
Expand Down Expand Up @@ -2974,21 +2995,32 @@ private extension CodexAccount {
return "Limited"
}

switch recoveryActionKind {
case .login:
return "Re-login"
case .refresh:
return "Refresh needed"
case .retryProbe:
return "Probe failed"
case .none:
break
}

switch status {
case "available":
return nil
case "usage_limited":
return "Limited"
case "probe_failed":
return "-"
return "Probe failed"
case "expired":
return "Refresh needed"
case "disabled":
return "Disabled"
case "cooldown":
return "Cooling"
case "unusable":
return "Needs login"
return "Needs attention"
default:
let label = status.replacingOccurrences(of: "_", with: " ").capitalized
return label.isEmpty ? nil : label
Expand Down
37 changes: 34 additions & 3 deletions apps/decodex-app/Sources/DecodexApp/LoginSheetView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,22 @@ private struct LoginCodeBoxesView: View {
Text(character(at: index))
.font(LoginFont.code)
.monospacedDigit()
.foregroundStyle(LoginPalette.primaryText(colorScheme))
.frame(width: 22, height: 30)
.modernGlassSurface(cornerRadius: 7, depth: .control)
.foregroundStyle(code.isEmpty ? LoginPalette.secondaryText(colorScheme).opacity(0.42) : LoginPalette.primaryText(colorScheme))
.frame(width: 23, height: 31)
.background {
RoundedRectangle(cornerRadius: 7, style: .continuous)
.fill(LoginPalette.codeBoxFill(colorScheme))
}
.overlay {
RoundedRectangle(cornerRadius: 7, style: .continuous)
.strokeBorder(LoginPalette.codeBoxStroke(colorScheme), lineWidth: 0.75)
.allowsHitTesting(false)
}
.shadow(
color: LoginPalette.codeBoxShadow(colorScheme),
radius: colorScheme == .dark ? 2.5 : 1.4,
y: 0.7
)
}
}
.frame(maxWidth: .infinity, alignment: .center)
Expand Down Expand Up @@ -478,4 +491,22 @@ private enum LoginPalette {
? Color(red: 0.86, green: 0.93, blue: 1)
: Color(red: 0.13, green: 0.32, blue: 0.52)
}

static func codeBoxFill(_ colorScheme: ColorScheme) -> Color {
colorScheme == .dark
? Color(red: 0.08, green: 0.1, blue: 0.14).opacity(0.72)
: Color(red: 0.96, green: 0.975, blue: 1).opacity(0.92)
}

static func codeBoxStroke(_ colorScheme: ColorScheme) -> Color {
colorScheme == .dark
? Color.white.opacity(0.16)
: Color(red: 0.48, green: 0.55, blue: 0.64).opacity(0.3)
}

static func codeBoxShadow(_ colorScheme: ColorScheme) -> Color {
colorScheme == .dark
? Color.black.opacity(0.22)
: Color(red: 0.24, green: 0.32, blue: 0.42).opacity(0.08)
}
}
76 changes: 70 additions & 6 deletions apps/decodex-app/Sources/DecodexApp/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
let note: String?
let planType: String?
let capacityMultiplier: Int?
let recoveryAction: String?
let refreshStatus: String?
let checkedAtUnixEpoch: Int?
let primaryWindowSeconds: Int?
Expand Down Expand Up @@ -159,15 +160,15 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
}

var needsLogin: Bool {
status == "unusable" || status == "expired" || !refreshTokenPresent
recoveryActionKind == .login
}

var canUseInCodex: Bool {
!disabled && !needsLogin
!disabled && recoveryActionKind != .login
}

var canRouteRuns: Bool {
!disabled && !needsLogin
!disabled && recoveryActionKind != .login
}

var statusLabel: String {
Expand All @@ -180,6 +181,14 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
if selected {
return "Runs routed"
}
switch recoveryActionKind {
case .login:
return "Re-login required"
case .retryProbe:
return "Probe failed"
case .refresh, .none:
break
}

switch status {
case "available": return "Ready"
Expand All @@ -188,7 +197,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
case "expired": return "Refresh needed"
case "disabled": return "Disabled"
case "cooldown": return "Cooling"
case "unusable": return "Needs login"
case "unusable": return recoveryActionKind == .login ? "Re-login required" : "Needs attention"
default: return status.replacingOccurrences(of: "_", with: " ").capitalized
}
}
Expand All @@ -206,10 +215,18 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
if selected {
return .selected
}
switch recoveryActionKind {
case .login:
return .danger
case .refresh, .retryProbe:
return .warning
case .none:
break
}
switch status {
case "available": return .ready
case "cooldown": return .warning
case "expired", "unusable", "disabled": return .danger
case "cooldown", "expired", "probe_failed": return .warning
case "unusable", "disabled": return .danger
default: return .neutral
}
}
Expand All @@ -222,6 +239,17 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
"\(capacityWeight)x"
}

var currentCapacityLabel: String? {
guard status == "available" || status == "usage_limited" else {
return nil
}
guard checkedAtUnixEpoch != nil || hasUsageWindowData else {
return nil
}

return capacityLabel
}

var hasUsageWindowData: Bool {
primaryRemainingPercent != nil || secondaryRemainingPercent != nil
}
Expand Down Expand Up @@ -276,6 +304,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
note: note,
planType: planType,
capacityMultiplier: capacityMultiplier,
recoveryAction: recoveryAction,
refreshStatus: refreshStatus,
checkedAtUnixEpoch: checkedAtUnixEpoch,
primaryWindowSeconds: primaryWindowSeconds,
Expand Down Expand Up @@ -312,6 +341,7 @@ struct CodexAccount: Decodable, Identifiable, Equatable {
case note
case planType = "plan_type"
case capacityMultiplier = "capacity_multiplier"
case recoveryAction = "recovery_action"
case refreshStatus = "refresh_status"
case checkedAtUnixEpoch = "checked_at_unix_epoch"
case primaryWindowSeconds = "primary_window_seconds"
Expand Down Expand Up @@ -340,6 +370,33 @@ struct CodexAccount: Decodable, Identifiable, Equatable {

return 1
}

var recoveryActionKind: AccountRecoveryAction {
if let recoveryAction = AccountRecoveryAction(rawValue: normalized(recoveryAction)) {
return recoveryAction
}
if !refreshTokenPresent {
return .login
}
if normalized(refreshStatus) == "failed" {
let noteText = normalized(note)
return noteText.contains("401") || noteText.contains("unauthorized") ? .login : .retryProbe
}
switch normalized(status) {
case "expired":
return .refresh
case "unusable":
return .login
case "probe_failed":
return .retryProbe
default:
return .none
}
}

private func normalized(_ value: String?) -> String {
value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
}
}

enum AccountTone {
Expand All @@ -351,6 +408,13 @@ enum AccountTone {
case neutral
}

enum AccountRecoveryAction: String {
case none
case refresh
case login
case retryProbe = "retry_probe"
}

enum UsageWindowLabel {
static func make(seconds: Int?) -> String {
guard let seconds, seconds > 0 else {
Expand Down
Loading