2026-10-17 09:33:27 -07:00
|
|
|
import ByreAPI
|
2026-10-17 07:26:18 -07:00
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
typealias AgentProcess = Components.Schemas.AgentProcess
|
|
|
|
|
typealias Zombie = Components.Schemas.Zombie
|
|
|
|
|
|
|
|
|
|
private let refreshKinds: Set<String> = ["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]
|
|
|
|
|
|
|
|
|
|
private let zombieLabel: [Zombie.KindPayload: String] = [
|
|
|
|
|
.runningWithoutProcess: "Running with no process",
|
|
|
|
|
.processWithoutSession: "Process with no session",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
private func since(_ date: Date) -> String {
|
|
|
|
|
let seconds = max(0, Int(Date().timeIntervalSince(date).rounded()))
|
|
|
|
|
if seconds < 60 {
|
|
|
|
|
return "\(seconds)s ago"
|
|
|
|
|
}
|
|
|
|
|
if seconds < 3600 {
|
|
|
|
|
return "\(seconds / 60)m \(seconds % 60)s ago"
|
|
|
|
|
}
|
|
|
|
|
return ago(date)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func bytes(_ n: Int64) -> String {
|
|
|
|
|
let units = ["B", "KB", "MB", "GB", "TB"]
|
|
|
|
|
var i = 0
|
|
|
|
|
var v = Double(n)
|
|
|
|
|
while v >= 1024 && i < units.count - 1 {
|
|
|
|
|
v /= 1024
|
|
|
|
|
i += 1
|
|
|
|
|
}
|
2026-10-17 09:57:28 -07:00
|
|
|
return "\(v.formatted(.number.precision(.fractionLength(i == 0 ? 0 : 1)).grouping(.never))) \(units[i])"
|
2026-10-17 07:26:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func resetText(_ unixSeconds: Double) -> String {
|
|
|
|
|
let at = Date(timeIntervalSince1970: unixSeconds)
|
|
|
|
|
let time = at.formatted(Date.FormatStyle().weekday(.abbreviated).hour().minute())
|
|
|
|
|
let minutes = Int((at.timeIntervalSinceNow / 60).rounded())
|
|
|
|
|
if minutes <= 0 {
|
|
|
|
|
return "\(time) (passed)"
|
|
|
|
|
}
|
|
|
|
|
let rel = minutes < 60 ? "\(minutes)m" : minutes < 1440 ? "\(minutes / 60)h \(minutes % 60)m" : "\(minutes / 1440)d \((minutes % 1440) / 60)h"
|
|
|
|
|
return "\(time) (in \(rel))"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func number(_ v: (any Sendable)??) -> Double? {
|
|
|
|
|
switch v {
|
|
|
|
|
case .some(.some(let x)):
|
|
|
|
|
if let d = x as? Double {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
if let i = x as? Int {
|
|
|
|
|
return Double(i)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct StatusView: View {
|
|
|
|
|
@Environment(Store.self) private var store
|
|
|
|
|
@ViewState private var status: ServerStatus?
|
|
|
|
|
@ViewState private var error = ""
|
|
|
|
|
@ViewState private var inFlight = false
|
|
|
|
|
@ViewState private var again = false
|
|
|
|
|
@ViewState private var off: (() -> Void)?
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
Group {
|
|
|
|
|
if let status {
|
|
|
|
|
ScrollView {
|
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
if !error.isEmpty {
|
|
|
|
|
Text(error).foregroundStyle(Palette.danger).padding(.top, 8)
|
|
|
|
|
}
|
|
|
|
|
Problems(status: status)
|
|
|
|
|
section("Running turns")
|
|
|
|
|
TurnList(status: status)
|
|
|
|
|
section("Rate limits")
|
|
|
|
|
let vendors = status.rateLimits.additionalProperties.keys.sorted()
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
ForEach(vendors, id: \.self) { v in
|
|
|
|
|
RateLimitCard(vendor: v, info: status.rateLimits.additionalProperties[v]!.value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if vendors.isEmpty {
|
|
|
|
|
Text("No vendor has reported a rate-limit status since byre started.").foregroundStyle(Palette.muted)
|
|
|
|
|
}
|
|
|
|
|
section("Server")
|
|
|
|
|
ServerRows(status: status)
|
|
|
|
|
}
|
|
|
|
|
.padding(.horizontal, 16)
|
|
|
|
|
.padding(.top, 8)
|
|
|
|
|
.padding(.bottom, 16)
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
EmptyText(text: error.isEmpty ? "Loading…" : error, error: !error.isEmpty)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
|
.background(Palette.bg)
|
|
|
|
|
.onAppear {
|
|
|
|
|
off = store.events.add { ev in
|
|
|
|
|
event(ev)
|
|
|
|
|
}
|
|
|
|
|
refresh()
|
|
|
|
|
}
|
|
|
|
|
.onDisappear {
|
|
|
|
|
off?()
|
|
|
|
|
off = nil
|
|
|
|
|
}
|
|
|
|
|
.onChange(of: store.connected) {
|
|
|
|
|
if store.connected {
|
|
|
|
|
refresh()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func section(_ title: String) -> some View {
|
|
|
|
|
Text(title)
|
|
|
|
|
.font(Fonts.font(13, weight: 700))
|
|
|
|
|
.padding(.top, 16)
|
|
|
|
|
.padding(.bottom, 8)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func refresh() {
|
|
|
|
|
if inFlight {
|
|
|
|
|
again = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
inFlight = true
|
|
|
|
|
Task {
|
|
|
|
|
do {
|
|
|
|
|
status = try await store.api.client.getStatus().ok.body.json
|
|
|
|
|
error = ""
|
|
|
|
|
} catch {
|
|
|
|
|
self.error = "Couldn't load status: \(errorText(error))"
|
|
|
|
|
}
|
|
|
|
|
inFlight = false
|
|
|
|
|
if again {
|
|
|
|
|
again = false
|
|
|
|
|
refresh()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func event(_ ev: ByreEvent) {
|
|
|
|
|
if refreshKinds.contains(ev.kind) || (ev.kind == "agent.vendor" && ev.payload.value["type"] as? String == "rate_limit_event") {
|
|
|
|
|
refresh()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard var s = status else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var changed = false
|
|
|
|
|
if let i = s.agentProcesses.firstIndex(where: { $0.sessionId == ev.agentSessionId }), ev.createdAt.timeIntervalSince(s.agentProcesses[i].lastEventAt) >= 1 {
|
|
|
|
|
s.agentProcesses[i].lastEventAt = ev.createdAt
|
|
|
|
|
changed = true
|
|
|
|
|
}
|
|
|
|
|
if ev.seq > s.lastEventSeq {
|
|
|
|
|
s.lastEventSeq = ev.seq
|
|
|
|
|
changed = true
|
|
|
|
|
}
|
|
|
|
|
if changed {
|
|
|
|
|
status = s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct TaskLinkText: View {
|
|
|
|
|
let taskId: String
|
|
|
|
|
@Environment(Store.self) private var store
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
let task = store.findTask(taskId)
|
|
|
|
|
let project = task.flatMap { t in store.projects.first { $0.id == t.projectId } }
|
|
|
|
|
Button {
|
|
|
|
|
store.focusTask(taskId)
|
|
|
|
|
} label: {
|
|
|
|
|
Text("\(project.map { "\($0.name) ▸ " } ?? "")\(task?.title ?? taskId)")
|
|
|
|
|
.foregroundStyle(store.taskColor(taskId) ?? Palette.accent)
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct Problems: View {
|
|
|
|
|
let status: ServerStatus
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
let pe = status.protocolErrors
|
|
|
|
|
let vendors = pe.counts.additionalProperties.keys.sorted()
|
|
|
|
|
if !status.zombies.isEmpty || !vendors.isEmpty {
|
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
|
|
Text("Problems")
|
|
|
|
|
.font(Fonts.font(13, weight: 700))
|
|
|
|
|
.padding(.top, 16)
|
|
|
|
|
.padding(.bottom, 4)
|
|
|
|
|
ForEach(Array(status.zombies.enumerated()), id: \.offset) { _, z in
|
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
|
|
|
|
Text(zombieLabel[z.kind]!).foregroundStyle(Palette.danger)
|
|
|
|
|
if let taskId = z.taskId {
|
|
|
|
|
TaskLinkText(taskId: taskId)
|
|
|
|
|
}
|
|
|
|
|
Text(z.detail).foregroundStyle(Palette.muted).textSelection(.enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ForEach(vendors, id: \.self) { v in
|
|
|
|
|
let n = pe.counts.additionalProperties[v] ?? 0
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text("\(v): \(n) bad stream line\(n == 1 ? "" : "s")").foregroundStyle(Palette.danger)
|
|
|
|
|
if let latest = pe.recent.first(where: { $0.vendor == v }) {
|
|
|
|
|
DetailsBox(boxed: false) {
|
|
|
|
|
Text("latest \(since(latest.at)): \(latest.reason)").foregroundStyle(Palette.muted)
|
|
|
|
|
} content: {
|
|
|
|
|
PreBox(text: latest.raw + (latest.raw.utf8.count < latest.size ? "… (\(bytes(Int64(latest.size))))" : ""), maxHeight: 180, size: 11)
|
|
|
|
|
.padding(.top, 4)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct TurnList: View {
|
|
|
|
|
let status: ServerStatus
|
|
|
|
|
@Environment(Store.self) private var store
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
ForEach(status.runningTurns, id: \.sessionId) { t in
|
|
|
|
|
let task = store.findTask(t.taskId)
|
|
|
|
|
let project = task.flatMap { tk in store.projects.first { $0.id == tk.projectId } }
|
|
|
|
|
let color = store.taskColor(t.taskId) ?? Palette.accent
|
|
|
|
|
HoverRow {
|
|
|
|
|
store.focusTask(t.taskId)
|
|
|
|
|
} content: {
|
|
|
|
|
StateDot(filled: Palette.ok).help("running")
|
|
|
|
|
HStack(spacing: 0) {
|
|
|
|
|
if let project {
|
|
|
|
|
Text("\(project.name) ▸ ").foregroundStyle(Palette.muted)
|
|
|
|
|
}
|
|
|
|
|
Text(task?.title ?? t.taskId).foregroundStyle(color)
|
|
|
|
|
}
|
|
|
|
|
.lineLimit(1)
|
|
|
|
|
.truncationMode(.tail)
|
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
|
if let p = status.agentProcesses.first(where: { $0.sessionId == t.sessionId }) {
|
|
|
|
|
Text(p.health == .ok ? since(p.lastEventAt) : "\(p.health.rawValue) · \(since(p.lastEventAt))")
|
|
|
|
|
.font(Fonts.font(11))
|
|
|
|
|
.foregroundStyle(p.health == .ok ? Palette.muted : Palette.danger)
|
|
|
|
|
}
|
|
|
|
|
Text(t.vendor).font(Fonts.font(11)).foregroundStyle(Palette.muted)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if status.runningTurns.isEmpty {
|
|
|
|
|
Text("No turns running.")
|
|
|
|
|
.foregroundStyle(Palette.muted)
|
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
|
.padding(.vertical, 6)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct RateLimitCard: View {
|
|
|
|
|
let vendor: String
|
|
|
|
|
let info: JSONDict
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
let statusText = info["status"].map { jsString($0) } ?? "unknown"
|
|
|
|
|
let tone = statusText == "allowed" ? Palette.ok : statusText == "rejected" ? Palette.danger : Palette.warn
|
|
|
|
|
let windows = dict(info, "unifiedWindows") ?? [:]
|
|
|
|
|
let rest = info.filter { !["status", "rateLimitType", "resetsAt", "unifiedWindows"].contains($0.key) }.sorted { $0.key < $1.key }
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
|
|
|
|
Text(vendor).font(Fonts.font(13, weight: 700)).foregroundStyle(Palette.text)
|
|
|
|
|
Text(statusText).font(Fonts.font(11, weight: 700)).foregroundStyle(tone)
|
|
|
|
|
if let type = info["rateLimitType"] {
|
|
|
|
|
Text(jsString(type))
|
|
|
|
|
}
|
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
|
if let resets = number(info["resetsAt"]) {
|
|
|
|
|
Text("resets \(resetText(resets))")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.font(Fonts.font(11))
|
|
|
|
|
.foregroundStyle(Palette.muted)
|
|
|
|
|
if !windows.isEmpty {
|
|
|
|
|
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 2) {
|
|
|
|
|
GridRow {
|
|
|
|
|
ForEach(["Window", "Used", "Elapsed", "Pace", "Resets"], id: \.self) { h in
|
|
|
|
|
Text(h).font(Fonts.font(11)).foregroundStyle(Palette.muted)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Divider().overlay(Palette.border)
|
|
|
|
|
ForEach(windows.keys.sorted(), id: \.self) { name in
|
|
|
|
|
let w = dict(windows, name) ?? [:]
|
|
|
|
|
let used = number(w["utilization"])
|
|
|
|
|
let elapsed = elapsedFraction(w)
|
|
|
|
|
GridRow {
|
|
|
|
|
Text(name)
|
|
|
|
|
Text(used.map { "\(Int(($0 * 100).rounded()))%" } ?? "—")
|
|
|
|
|
.foregroundStyle(used.map { $0 >= 0.9 ? Palette.danger : $0 >= 0.7 ? Palette.warn : Palette.text } ?? Palette.text)
|
|
|
|
|
Text(elapsed.map { "\(Int(($0 * 100).rounded()))%" } ?? "—")
|
|
|
|
|
PaceBar(used: used, elapsed: elapsed)
|
|
|
|
|
.gridColumnAlignment(.leading)
|
|
|
|
|
Text(number(w["resetsAt"]).map(resetText) ?? "—")
|
|
|
|
|
}
|
|
|
|
|
Divider().overlay(Palette.border)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !rest.isEmpty {
|
|
|
|
|
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 2) {
|
|
|
|
|
ForEach(rest, id: \.key) { k, v in
|
|
|
|
|
GridRow {
|
|
|
|
|
Text(k).foregroundStyle(Palette.muted)
|
|
|
|
|
Text(v is JSONDict || v is [(any Sendable)?] ? compactJSONText(v) : jsString(v)).textSelection(.enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func elapsedFraction(_ w: JSONDict) -> Double? {
|
|
|
|
|
guard let resets = number(w["resetsAt"]), let mins = number(w["windowMins"]) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
let remaining = resets * 1000 - Date().timeIntervalSince1970 * 1000
|
|
|
|
|
return min(1, max(0, 1 - remaining / (mins * 60000)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func compactJSONText(_ v: (any Sendable)?) -> String {
|
|
|
|
|
prettyJSON(v).components(separatedBy: "\n").map { trimmed($0) }.joined(separator: " ")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct PaceBar: View {
|
|
|
|
|
let used: Double?
|
|
|
|
|
let elapsed: Double?
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
if let used, let elapsed {
|
|
|
|
|
let diff = Int(((used - elapsed) * 100).rounded())
|
|
|
|
|
let title = diff > 0 ? "\(diff) points ahead of an even pace" : diff < 0 ? "\(-diff) points behind an even pace" : "on an even pace"
|
|
|
|
|
GeometryReader { g in
|
|
|
|
|
ZStack(alignment: .leading) {
|
|
|
|
|
RoundedRectangle(cornerRadius: 2).fill(Palette.surface)
|
|
|
|
|
Rectangle()
|
|
|
|
|
.fill((used > elapsed ? Palette.warn : Palette.ok).opacity(0.55))
|
|
|
|
|
.frame(width: g.size.width * min(1, used))
|
|
|
|
|
RoundedRectangle(cornerRadius: 2).stroke(Palette.border)
|
|
|
|
|
Rectangle()
|
|
|
|
|
.fill(Palette.text)
|
|
|
|
|
.frame(width: 2, height: g.size.height + 6)
|
|
|
|
|
.offset(x: g.size.width * elapsed - 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.frame(minWidth: 80, maxWidth: .infinity)
|
|
|
|
|
.frame(height: 10)
|
|
|
|
|
.help(title)
|
|
|
|
|
} else {
|
|
|
|
|
Text("—")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct ServerRows: View {
|
|
|
|
|
let status: ServerStatus
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
let rows: [(String, String)] = [
|
|
|
|
|
("Started", "\(status.startedAt.formatted(date: .numeric, time: .standard)) (\(ago(status.startedAt)))"),
|
|
|
|
|
("Agent processes", String(status.liveAgents)),
|
|
|
|
|
("Projects", String(status.projects)),
|
|
|
|
|
("Active tasks", String(status.activeTasks)),
|
|
|
|
|
("Live clients", String(status.sseClients)),
|
|
|
|
|
("Database", bytes(status.dbBytes)),
|
|
|
|
|
("Last event", "#\(status.lastEventSeq)"),
|
|
|
|
|
]
|
|
|
|
|
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 2) {
|
|
|
|
|
ForEach(rows, id: \.0) { k, v in
|
|
|
|
|
GridRow {
|
|
|
|
|
Text(k).foregroundStyle(Palette.muted)
|
|
|
|
|
Text(v).textSelection(.enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.top, 8)
|
|
|
|
|
}
|
|
|
|
|
}
|