Skip to content
Merged
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
150 changes: 124 additions & 26 deletions apps/decodex-app/Sources/DecodexApp/AccountPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2890,9 +2890,9 @@ struct OperatorLanePopoverView: View {
}
}

if let continuationRecoveryReadout {
if let continuationRecovery = run.continuationRecovery {
measuredReadout {
OperatorLaneReadoutRow(title: "Recovery", items: continuationRecoveryReadout)
OperatorLaneRecoveryReadout(recovery: continuationRecovery)
}
}
}
Expand Down Expand Up @@ -2986,30 +2986,6 @@ struct OperatorLanePopoverView: View {
panelTrimmed(run.projectDisplayName) ?? panelTrimmed(run.projectID)
}

private var continuationRecoveryReadout: [OperatorLaneReadoutItem]? {
guard let recovery = run.continuationRecovery else {
return nil
}

let state = rawPanelToken(recovery.state ?? "continuation")
let phase = [
recovery.sourcePhase.map(rawPanelToken),
recovery.nextPhase.map(rawPanelToken),
]
.compactMap { $0 }
.joined(separator: " -> ")
let count = "\(recovery.recoveryCount ?? 0)/\(recovery.automaticContinuationLimit ?? 0)"
let budget = recovery.budgetExceeded == true ? "exceeded" : "within"
let error = rawPanelToken(recovery.sourceErrorClass ?? "unknown")

return [
OperatorLaneReadoutItem(label: "state", value: state),
OperatorLaneReadoutItem(label: "phase", value: phase.isEmpty ? "unknown" : phase),
OperatorLaneReadoutItem(label: "count", value: "\(count) \(budget)"),
OperatorLaneReadoutItem(label: "error", value: error),
]
}

private var alignedWidth: CGFloat? {
readoutWidth > 0 ? readoutWidth : nil
}
Expand Down Expand Up @@ -3888,6 +3864,128 @@ struct OperatorLaneReadoutRow: View {
}
}

fileprivate struct OperatorLaneRecoveryReadout: View {
let recovery: OperatorContinuationRecoveryStatus
@State private var isExpanded = false
@Environment(\.colorScheme) private var colorScheme

var body: some View {
VStack(alignment: .leading, spacing: OperatorLaneReadoutLayout.itemRowSpacing) {
Button {
isExpanded.toggle()
} label: {
HStack(alignment: .firstTextBaseline, spacing: OperatorLaneReadoutLayout.columnSpacing) {
OperatorLaneReadoutLabelView(title: "Recovery")

OperatorLaneReadoutSummaryView(fragments: summaryFragments)
.lineLimit(1)
.allowsTightening(true)
.fixedSize(horizontal: true, vertical: false)

Image(systemName: isExpanded ? "chevron.down" : "chevron.right")
.font(.system(size: 7.5, weight: .semibold))
.foregroundStyle(OperatorLanePopoverStyle.mutedText(colorScheme))
.frame(width: 8, height: 8)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.help(accessibilityText)

if isExpanded {
VStack(alignment: .leading, spacing: OperatorLaneReadoutLayout.itemRowSpacing) {
ForEach(detailItems) { item in
OperatorLaneRecoveryDetailRow(item: item)
}
}
.padding(.leading, OperatorLaneReadoutLayout.titleWidth + OperatorLaneReadoutLayout.columnSpacing)
.padding(.top, 1)
}
}
.fixedSize(horizontal: true, vertical: false)
.accessibilityElement(children: .combine)
.accessibilityLabel(accessibilityText)
}

private var summaryFragments: [[OperatorLaneReadoutTextRun]] {
var fragments = [[OperatorLaneReadoutTextRun]]()
fragments.append([.value(token(recovery.state, fallback: "continuation"))])
if recovery.budgetExceeded == true {
fragments.append([.meta("budget "), .value("exceeded")])
}

return fragments
}

private var detailItems: [OperatorLaneReadoutItem] {
[
OperatorLaneReadoutItem(label: "state", value: token(recovery.state, fallback: "continuation")),
OperatorLaneReadoutItem(label: "phase", value: phaseValue),
OperatorLaneReadoutItem(label: "count", value: countValue),
OperatorLaneReadoutItem(label: "error", value: token(recovery.sourceErrorClass, fallback: "unknown")),
]
}

private var phaseValue: String {
let phases = [
tokenOrNil(recovery.sourcePhase),
tokenOrNil(recovery.nextPhase),
].compactMap { $0 }

return phases.isEmpty ? "unknown" : phases.joined(separator: " -> ")
}

private var countValue: String {
let count = recovery.recoveryCount ?? 0
let limit = recovery.automaticContinuationLimit ?? 0
let budget = recovery.budgetExceeded == true ? "exceeded" : "within"

return "\(count)/\(limit) \(budget)"
}

private var accessibilityText: String {
detailItems.map(\.accessibilityText)
.joined(separator: ", ")
}

private func token(_ value: String?, fallback: String) -> String {
tokenOrNil(value) ?? fallback
}

private func tokenOrNil(_ value: String?) -> String? {
guard let value else {
return nil
}

let token = rawPanelToken(value)

return token.isEmpty ? nil : token
}
}

fileprivate struct OperatorLaneRecoveryDetailRow: View {
let item: OperatorLaneReadoutItem
@Environment(\.colorScheme) private var colorScheme

var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(item.label ?? "value")
.font(OperatorLanePopoverStyle.metaFont)
.foregroundStyle(OperatorLanePopoverStyle.mutedText(colorScheme))
.frame(width: 38, alignment: .leading)

Text(item.displayValue)
.font(OperatorLanePopoverStyle.valueFont)
.foregroundStyle(OperatorLanePopoverStyle.primaryText(colorScheme))
.monospacedDigit()
.lineLimit(1)
.truncationMode(.middle)
.frame(maxWidth: 340, alignment: .leading)
.help(item.accessibilityText)
}
}
}

fileprivate struct OperatorLaneReadoutSummaryView: View {
let fragments: [[OperatorLaneReadoutTextRun]]
@Environment(\.colorScheme) private var colorScheme
Expand Down