2026-10-17 10:13:26 -07:00
import CoreData
2026-10-16 22:46:54 -07:00
import Foundation
import Observation
2026-10-17 10:13:26 -07:00
import SwiftData
2026-10-16 22:46:54 -07:00
import SwiftUI
@ MainActor
@ Observable
2026-10-18 20:58:56 -07:00
final class Servers {
static let shared = Servers ()
2026-10-16 22:46:54 -07:00
2026-10-18 20:58:56 -07:00
private ( set ) var entries : [( origin : String , record : ServerRecord )] = []
private ( set ) var connections : [ String : Connection ] = [:]
@ ObservationIgnored var shown : Set < String > = []
@ ObservationIgnored var launched = false
@ ObservationIgnored var openWindow : (( String ?) -> Void )?
2026-10-16 22:46:54 -07:00
2026-10-18 20:58:56 -07:00
private init () {
load ()
2026-10-17 10:13:26 -07:00
Task {
for await note in NotificationCenter . default . notifications ( named : NSPersistentCloudKitContainer . eventChangedNotification ) {
if let event = note . userInfo ?[ NSPersistentCloudKitContainer . eventNotificationUserInfoKey ] as ? NSPersistentCloudKitContainer . Event , event . type == . import , event . endDate != nil {
2026-10-18 20:58:56 -07:00
for connection in connections . values {
connection . store ?. drafts ?. reload ()
}
load ()
2026-10-17 10:13:26 -07:00
}
2026-10-16 22:46:54 -07:00
}
}
}
2026-10-18 20:58:56 -07:00
var origins : [ String ] {
entries . map ( \ . origin )
}
func record ( _ origin : String ) -> ServerRecord ? {
entries . first { $0 . origin == origin }?. record
}
func color ( _ origin : String ) -> Color {
taskPalette [( record ( origin )?. color ?? 0 ) % taskPalette . count ]
}
private func load () {
var loaded : [( origin : String , record : ServerRecord )] = []
for record in fetchRecords ( FetchDescriptor < ServerRecord >( sortBy : [ SortDescriptor ( \ . saved )])) {
if let origin = parseOrigin ( record . origin ), ! loaded . contains ( where : { $0 . origin == origin . absoluteString }) {
loaded . append (( origin . absoluteString , record ))
}
2026-10-17 10:13:26 -07:00
}
2026-10-18 20:58:56 -07:00
entries = loaded
for ( origin , _ ) in loaded where connections [ origin ] == nil {
connections [ origin ] = Connection ( origin : parseOrigin ( origin ) ! )
}
for ( origin , connection ) in connections where ! loaded . contains ( where : { $0 . origin == origin }) {
connections [ origin ] = nil
Task {
await connection . stop ()
}
2026-10-17 10:13:26 -07:00
}
}
2026-10-18 20:58:56 -07:00
func add ( _ origin : URL ) {
if record ( origin . absoluteString ) == nil {
let used = Set ( entries . map ( \ . record . color ))
let color = taskPalette . indices . first { ! used . contains ( $0 ) } ?? entries . count % taskPalette . count
persistence . insert ( ServerRecord ( origin : origin . absoluteString , color : color ))
saveRecords ()
}
load ()
}
func remove ( _ origin : String ) {
for record in fetchRecords ( FetchDescriptor < ServerRecord >()) where parseOrigin ( record . origin )?. absoluteString == origin {
persistence . delete ( record )
}
saveRecords ()
load ()
}
func setColor ( _ origin : String , _ color : Int ) {
for record in fetchRecords ( FetchDescriptor < ServerRecord >()) where parseOrigin ( record . origin )?. absoluteString == origin {
record . color = color
}
saveRecords ()
}
2026-10-18 21:40:27 -07:00
func step ( from origin : String ?, by step : Int ) -> String ? {
guard let i = origin . flatMap ({ origins . firstIndex ( of : $0 ) }) else {
return origins . first
}
return origins [( i + step + origins . count ) % origins . count ]
}
2026-10-18 20:58:56 -07:00
func openAll () {
if origins . isEmpty {
openWindow ?( nil )
}
for origin in origins {
openWindow ?( origin )
}
}
}
@ MainActor
@ Observable
final class Connection {
enum Phase {
case starting
case failed ( String )
case running ( Store )
}
let origin : URL
var phase : Phase = . starting
@ ObservationIgnored private var stopped = false
init ( origin : URL ) {
self . origin = origin
Task {
await start ()
}
2026-10-17 10:13:26 -07:00
}
2026-10-16 22:46:54 -07:00
var store : Store ? {
if case . running ( let store ) = phase {
return store
}
return nil
}
2026-10-18 20:58:56 -07:00
func stop () async {
stopped = true
if let store {
await store . stop ()
}
}
private func start () async {
2026-10-18 21:54:54 -07:00
while ! stopped , ! ( await attempt ()) {
try ? await Task . sleep ( for : . seconds ( 2 ))
}
}
private func attempt () async -> Bool {
2026-10-18 20:58:56 -07:00
let store = Store ( api : API ( origin : origin ))
do {
let status = try await store . api . client . getStatus (). ok . body . json
async let me : Void = store . loadMe ()
async let projects : Void = store . loadProjects ()
async let inbox : Void = store . loadInbox ()
async let lead : Void = store . loadLeadSessions ()
async let reads : Void = store . loadSummaryReads ()
_ = try await ( me , projects , inbox , lead , reads )
store . loadDrafts ()
try await store . loadFocus ()
guard ! stopped else {
await store . stop ()
2026-10-18 21:54:54 -07:00
return true
2026-10-18 20:58:56 -07:00
}
Task {
await store . startLeadStream ()
}
phase = . running ( store )
store . startGlobalStream ( after : status . lastEventSeq )
store . remote . start ()
Notifier . shared . deliver ()
2026-10-18 21:54:54 -07:00
return true
2026-10-18 20:58:56 -07:00
} catch {
await store . stop ()
if ! stopped {
phase = . failed ( "byre failed to start: \( errorText ( error )) " )
}
2026-10-18 21:54:54 -07:00
return false
2026-10-18 20:58:56 -07:00
}
}
}
@ MainActor
@ Observable
final class ConnectForm {
var serverText = ""
var problem : String ?
var connecting = false
func connect () async -> URL ? {
2026-10-16 22:46:54 -07:00
guard let origin = parseOrigin ( serverText ) else {
problem = "The server URL must be an https origin with no path, such as https://byre.example.ts.net."
2026-10-18 20:58:56 -07:00
return nil
2026-10-16 22:46:54 -07:00
}
connecting = true
problem = nil
do {
_ = try await API ( origin : origin ). client . getStatus (). ok
} catch {
2026-10-18 20:58:56 -07:00
connecting = false
problem = explain ( error , origin )
return nil
2026-10-16 22:46:54 -07:00
}
connecting = false
2026-10-18 20:58:56 -07:00
Servers . shared . add ( origin )
return origin
2026-10-16 22:46:54 -07:00
}
2026-10-18 20:58:56 -07:00
func installHere () async -> URL ? {
2026-10-18 16:14:03 -07:00
connecting = true
problem = nil
do {
let origin = try await Server . turnOn ()
serverText = origin . absoluteString
try await waitForServer ( origin )
} catch {
2026-10-18 20:58:56 -07:00
connecting = false
problem = errorText ( error )
return nil
2026-10-18 16:14:03 -07:00
}
2026-10-18 20:58:56 -07:00
return await connect ()
2026-10-18 16:14:03 -07:00
}
private func waitForServer ( _ origin : URL ) async throws {
let api = API ( origin : origin )
try await withThrowingTaskGroup ( of : Void . self ) { group in
group . addTask {
while ( try ? await api . client . getStatus (). ok ) == nil {
try await Task . sleep ( for : . milliseconds ( 500 ))
}
}
group . addTask {
try await Task . sleep ( for : . seconds ( 10 ))
throw ProcessFailure ( errorDescription : "byre was installed but \( origin . absoluteString ) didn't answer within 10 s." )
}
defer {
group . cancelAll ()
}
try await group . next ()
}
}
2026-10-16 22:46:54 -07:00
}
private let tlsErrors : Set < URLError . Code > = [
. serverCertificateUntrusted , . serverCertificateHasBadDate , . serverCertificateNotYetValid , . serverCertificateHasUnknownRoot ,
. secureConnectionFailed , . clientCertificateRejected , . clientCertificateRequired ,
]
private let unreachableErrors : Set < URLError . Code > = [
. cannotFindHost , . cannotConnectToHost , . dnsLookupFailed , . timedOut , . notConnectedToInternet , . networkConnectionLost ,
]
func explain ( _ error : any Error , _ origin : URL ) -> String {
let host = origin . host () ?? origin . absoluteString
let e = rootError ( error )
if let api = e as ? ApiError {
switch api . status {
case 401 :
return " \( host ) answered 401, no Tailscale identity: \( api . message ) . Use byre's ts.net HTTPS address, the one tailscale serve gives it, so the request carries your Tailscale login."
case 403 :
return " \( host ) answered 403, the request didn't come through tailscale serve: \( api . message ) . Use the HTTPS address tailscale serve gives byre."
default :
return api . message
}
}
if let url = e as ? URLError {
if tlsErrors . contains ( url . code ) {
return " \( host ) 's certificate isn't trusted: \( url . localizedDescription ) Use byre's ts.net HTTPS address, whose certificate tailscale serve provides."
}
if unreachableErrors . contains ( url . code ) {
return "Can't reach \( host ) : \( url . localizedDescription ) Is Tailscale connected?"
}
return url . localizedDescription
}
return errorText ( e )
}
struct ConnectView : View {
2026-10-18 20:58:56 -07:00
@ Bindable var form : ConnectForm
let connected : ( URL ) -> Void
2026-10-16 22:46:54 -07:00
var body : some View {
VStack ( alignment : . leading , spacing : 10 ) {
Text ( "Server URL" )
2026-10-18 20:58:56 -07:00
TextField ( "https://byre.example.ts.net" , text : $ form . serverText )
2026-10-16 22:46:54 -07:00
. textFieldStyle (. roundedBorder )
. onSubmit ( connect )
2026-10-18 16:14:03 -07:00
HStack ( spacing : 10 ) {
Button ( "Connect" , action : connect )
Button ( "Run byre on this Mac" , action : installHere )
}
2026-10-18 20:58:56 -07:00
. disabled ( form . connecting )
if let problem = form . problem {
2026-10-16 22:46:54 -07:00
Text ( problem )
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
}
. frame ( width : 460 )
. frame ( maxWidth : . infinity , maxHeight : . infinity )
}
private func connect () {
Task {
2026-10-18 20:58:56 -07:00
if let origin = await form . connect () {
connected ( origin )
}
2026-10-16 22:46:54 -07:00
}
}
2026-10-18 16:14:03 -07:00
private func installHere () {
Task {
2026-10-18 20:58:56 -07:00
if let origin = await form . installHere () {
connected ( origin )
}
2026-10-18 16:14:03 -07:00
}
}
2026-10-16 22:46:54 -07:00
}
struct FatalView : View {
let message : String
var body : some View {
Text ( message )
. foregroundStyle ( Palette . danger )
. textSelection (. enabled )
. padding ( 24 )
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
}
}
2026-10-18 20:58:56 -07:00
struct ServerFocus {
2026-10-18 21:40:27 -07:00
let origin : String ?
2026-10-18 20:58:56 -07:00
let store : Store ?
let overlays : Overlays
let forget : (() -> Void )?
}
extension FocusedValues {
@ Entry var serverWindow : ServerFocus ?
}
struct ServerWindow : View {
@ Binding var origin : String ?
@ ViewState private var overlays = Overlays ()
@ ViewState private var form = ConnectForm ()
2026-10-17 08:24:22 -07:00
@ Environment ( \ . openWindow ) private var openWindow
2026-10-18 20:58:56 -07:00
@ Environment ( \ . dismiss ) private var dismissWindow
2026-10-16 22:46:54 -07:00
2026-10-18 20:58:56 -07:00
private var connection : Connection ? {
origin . flatMap { Servers . shared . connections [ $0 ] }
}
private var focus : ServerFocus {
var forgetting : (() -> Void )?
if connection != nil {
forgetting = forget
}
2026-10-18 21:40:27 -07:00
return ServerFocus ( origin : connection ?. origin . absoluteString , store : connection ?. store , overlays : overlays , forget : forgetting )
2026-10-18 20:58:56 -07:00
}
private var frameColor : Color {
connection . map { Servers . shared . color ( $0 . origin . absoluteString ) } ?? Palette . bg
}
@ ViewBuilder
private var content : some View {
if let connection {
2026-10-16 22:46:54 -07:00
switch connection . phase {
case . starting :
Color . clear
case . failed ( let message ):
FatalView ( message : message )
case . running ( let store ):
2026-10-16 23:30:57 -07:00
MainView ()
2026-10-16 22:46:54 -07:00
. environment ( store )
}
2026-10-18 20:58:56 -07:00
} else {
ConnectView ( form : form ) { connected in
let key = connected . absoluteString
if Servers . shared . shown . contains ( key ) {
openWindow ( id : "main" , value : key )
dismissWindow ()
return
}
origin = key
}
}
}
private var framed : some View {
content
. foregroundStyle ( Palette . text )
. tint ( Palette . accent )
. background ( Palette . bg )
2026-10-18 21:29:18 -07:00
. padding ( connection == nil ? 0 : 3 )
2026-10-18 20:58:56 -07:00
. background ( frameColor )
. environment ( overlays )
. font ( Fonts . font ( 13 ))
. frame ( minWidth : 640 , minHeight : 400 )
}
var body : some View {
framed
. navigationTitle ( connection ?. origin . host () ?? "byre" )
2026-10-18 21:40:27 -07:00
. background {
WindowReader ( found : joinTabs )
}
2026-10-18 20:58:56 -07:00
. focusedSceneValue ( \ . serverWindow , focus )
. onChange ( of : origin , initial : true ) { old , new in
moved ( old , new )
}
. onChange ( of : Servers . shared . origins ) { old , new in
adopt ( new . filter { ! old . contains ( $0 ) })
}
. onAppear ( perform : appeared )
. onDisappear {
if let origin {
Servers . shared . shown . remove ( origin )
}
}
}
private func moved ( _ old : String ?, _ new : String ?) {
if let old {
Servers . shared . shown . remove ( old )
}
if let new {
Servers . shared . shown . insert ( new )
2026-10-16 22:46:54 -07:00
}
2026-10-18 20:58:56 -07:00
}
private func appeared () {
let servers = Servers . shared
let openWindow = openWindow
servers . openWindow = { origin in
if let origin {
openWindow ( id : "main" , value : origin )
} else {
2026-10-17 08:24:22 -07:00
openWindow ( id : "main" )
}
}
2026-10-18 20:58:56 -07:00
if ! servers . launched {
servers . launched = true
adopt ( servers . origins )
for o in servers . origins where ! servers . shown . contains ( o ) {
openWindow ( id : "main" , value : o )
}
}
Notifier . shared . deliver ()
}
2026-10-18 21:40:27 -07:00
private func joinTabs ( _ window : NSWindow ) {
window . tabbingMode = . preferred
DispatchQueue . main . async {
let host = NSApp . windows . first { $0 !== window && $0 . isVisible && $0 . tabbingIdentifier == window . tabbingIdentifier }
if let host , ! ( host . tabbedWindows ?? []). contains ( window ) {
host . addTabbedWindow ( window , ordered : . above )
}
}
}
2026-10-18 20:58:56 -07:00
private func adopt ( _ candidates : [ String ]) {
guard origin == nil , form . serverText . isEmpty , ! form . connecting , let next = candidates . first ( where : { ! Servers . shared . shown . contains ( $0 ) }) else {
return
}
origin = next
Servers . shared . shown . insert ( next )
}
private func forget () {
guard let current = origin else {
return
}
form . serverText = current
origin = nil
Servers . shared . remove ( current )
2026-10-16 22:46:54 -07:00
}
}