55// Nova/Claude API — port 37424
66//
77// Endpoints:
8- // GET /api/status → app status, running jobs
8+ // GET /api/status → app status, job count
99// GET /api/jobs → list all sync jobs
1010// GET /api/jobs/:id → single job detail
11- // POST /api/jobs/:id/run → run a specific job
12- // POST /api/jobs/:id/stop → stop a running job
13- // POST /api/jobs → create new job
14- // GET /api/jobs/:id/history → execution history for job
15- // GET /api/history → all recent execution history
16- // POST /api/jobs/:id/test → dry-run test
11+ // POST /api/jobs/:id/run → execute a job
12+ // POST /api/jobs/:id/dryrun → dry-run test
13+ // GET /api/history → recent execution history
14+ // GET /api/jobs/:id/history → history for specific job
1715//
1816// Created by Jordan Koch on 2026.
1917// Copyright © 2026 Jordan Koch. All rights reserved.
@@ -59,29 +57,33 @@ class NovaAPIServer {
5957 private func route( _ req: NovaRequest ) async -> String {
6058 if req. method == " OPTIONS " { return http ( 200 , " " ) }
6159 let jm = JobManager . shared
60+ let hm = ExecutionHistoryManager . shared
6261
6362 switch ( req. method, req. path) {
6463
6564 case ( " GET " , " /api/status " ) :
66- let running = jm. jobs. filter { $0. currentStatus == . running }
6765 return json ( 200 , [
6866 " status " : " running " , " app " : " RsyncGUI " , " version " : " 1.0 " , " port " : " \( port) " ,
6967 " jobCount " : jm. jobs. count,
70- " runningJobs " : running . count,
68+ " enabledJobs " : jm . jobs . filter { $0 . isEnabled } . count,
7169 " uptimeSeconds " : Int ( Date ( ) . timeIntervalSince ( startTime) )
7270 ] )
7371
72+ case ( " GET " , " /api/ping " ) :
73+ return json ( 200 , [ " pong " : true ] )
74+
7475 case ( " GET " , " /api/jobs " ) :
7576 let jobs = jm. jobs. map { j -> [ String : Any ] in [
76- " id " : j. id. uuidString, " name " : j. name,
77- " source " : j. source, " destination " : j. destination,
78- " status " : j. currentStatus. rawValue,
77+ " id " : j. id. uuidString,
78+ " name " : j. name,
79+ " source " : j. source,
80+ " destination " : j. destination,
7981 " isEnabled " : j. isEnabled,
80- " lastRun " : j. lastExecutionDate . map { ISO8601DateFormatter ( ) . string ( from : $0 ) } ?? " "
82+ " syncMode " : j. syncMode . rawValue
8183 ] }
8284 return jsonArray ( 200 , jobs)
8385
84- case ( " GET " , _) where req. path. hasPrefix ( " /api/jobs/ " ) && !req. path. contains ( " /history " ) && !req . path . hasSuffix ( " /run " ) && !req. path. hasSuffix ( " /stop " ) && !req. path. hasSuffix ( " /test " ) :
86+ case ( " GET " , _) where req. path. hasPrefix ( " /api/jobs/ " ) && !req. path. hasSuffix ( " /run " ) && !req. path. hasSuffix ( " /dryrun " ) && !req. path. hasSuffix ( " /history " ) :
8587 let idStr = req. path. replacingOccurrences ( of: " /api/jobs/ " , with: " " )
8688 guard let uuid = UUID ( uuidString: idStr) ,
8789 let job = jm. jobs. first ( where: { $0. id == uuid } ) else {
@@ -90,9 +92,7 @@ class NovaAPIServer {
9092 return json ( 200 , [
9193 " id " : job. id. uuidString, " name " : job. name,
9294 " sources " : job. sources, " destination " : job. destination,
93- " status " : job. currentStatus. rawValue,
94- " isEnabled " : job. isEnabled,
95- " mode " : job. syncMode. rawValue
95+ " isEnabled " : job. isEnabled, " syncMode " : job. syncMode. rawValue
9696 ] as [ String : Any ] )
9797
9898 case ( " POST " , _) where req. path. hasSuffix ( " /run " ) :
@@ -101,49 +101,44 @@ class NovaAPIServer {
101101 let job = jm. jobs. first ( where: { $0. id == uuid } ) else {
102102 return json ( 404 , [ " error " : " Job not found " ] )
103103 }
104- await jm. runJob ( job)
104+ Task {
105+ _ = try ? await jm. executeJob ( job, dryRun: false )
106+ }
105107 return json ( 200 , [ " status " : " started " , " job " : job. name] )
106108
107- case ( " POST " , _) where req. path. hasSuffix ( " /stop " ) :
109+ case ( " POST " , _) where req. path. hasSuffix ( " /dryrun " ) :
108110 let idStr = req. path. components ( separatedBy: " / " ) . dropLast ( ) . last ?? " "
109111 guard let uuid = UUID ( uuidString: idStr) ,
110112 let job = jm. jobs. first ( where: { $0. id == uuid } ) else {
111113 return json ( 404 , [ " error " : " Job not found " ] )
112114 }
113- jm. stopJob ( job)
114- return json ( 200 , [ " status " : " stopped " , " job " : job. name] )
115-
116- case ( " GET " , _) where req. path. hasSuffix ( " /history " ) :
117- let idStr = req. path. components ( separatedBy: " / " ) . dropLast ( ) . last ?? " "
118- guard let uuid = UUID ( uuidString: idStr) else { return json ( 400 , [ " error " : " Invalid UUID " ] ) }
119- let history = ExecutionHistoryManager . shared. history ( for: uuid)
120- let entries = history. map { h -> [ String : Any ] in [
121- " id " : h. id. uuidString,
122- " startedAt " : ISO8601DateFormatter ( ) . string ( from: h. startedAt) ,
123- " duration " : h. duration,
124- " status " : h. status. rawValue,
125- " filesSynced " : h. filesSynced,
126- " bytesTransferred " : h. bytesTransferred
127- ] }
128- return jsonArray ( 200 , entries)
115+ Task {
116+ _ = try ? await jm. executeJob ( job, dryRun: true )
117+ }
118+ return json ( 200 , [ " status " : " dryrun_started " , " job " : job. name] )
129119
130120 case ( " GET " , " /api/history " ) :
131- let history = ExecutionHistoryManager . shared. recentHistory ( limit: 50 )
132- let entries = history. map { h -> [ String : Any ] in [
133- " id " : h. id. uuidString, " jobId " : h. jobId. uuidString,
134- " startedAt " : ISO8601DateFormatter ( ) . string ( from: h. startedAt) ,
135- " status " : h. status. rawValue, " filesSynced " : h. filesSynced
121+ let entries = hm. getAllHistory ( limit: 50 ) . map { e -> [ String : Any ] in [
122+ " id " : e. id. uuidString,
123+ " jobName " : e. jobName,
124+ " startTime " : ISO8601DateFormatter ( ) . string ( from: e. timestamp) ,
125+ " status " : e. status. rawValue,
126+ " filesTransferred " : e. filesTransferred,
127+ " bytesTransferred " : e. bytesTransferred
136128 ] }
137129 return jsonArray ( 200 , entries)
138130
139- case ( " POST " , _) where req. path. hasSuffix ( " /test " ) :
131+ case ( " GET " , _) where req. path. hasSuffix ( " /history " ) :
140132 let idStr = req. path. components ( separatedBy: " / " ) . dropLast ( ) . last ?? " "
141- guard let uuid = UUID ( uuidString: idStr) ,
142- let job = jm. jobs. first ( where: { $0. id == uuid } ) else {
143- return json ( 404 , [ " error " : " Job not found " ] )
144- }
145- await jm. dryRunJob ( job)
146- return json ( 200 , [ " status " : " test_complete " , " job " : job. name] )
133+ guard let uuid = UUID ( uuidString: idStr) else { return json ( 400 , [ " error " : " Invalid UUID " ] ) }
134+ let entries = hm. getHistory ( for: uuid) . map { e -> [ String : Any ] in [
135+ " id " : e. id. uuidString,
136+ " startTime " : ISO8601DateFormatter ( ) . string ( from: e. timestamp) ,
137+ " status " : e. status. rawValue,
138+ " filesTransferred " : e. filesTransferred,
139+ " bytesTransferred " : e. bytesTransferred
140+ ] }
141+ return jsonArray ( 200 , entries)
147142
148143 default :
149144 return json ( 404 , [ " error " : " Not found: \( req. method) \( req. path) " ] )
@@ -157,7 +152,7 @@ class NovaAPIServer {
157152 guard let raw = String ( data: data, encoding: . utf8) , raw. contains ( " \r \n \r \n " ) else { return nil }
158153 let parts = raw. components ( separatedBy: " \r \n \r \n " ) ; let lines = parts [ 0 ] . components ( separatedBy: " \r \n " )
159154 guard let rl = lines. first else { return nil } ; let tokens = rl. components ( separatedBy: " " ) ; guard tokens. count >= 2 else { return nil }
160- var hdrs : [ String : String ] = [ ] ; for l in lines. dropFirst ( ) { let kv = l. components ( separatedBy: " : " ) ; if kv. count >= 2 { hdrs [ kv [ 0 ] . lowercased ( ) ] = kv. dropFirst ( ) . joined ( separator: " : " ) } }
155+ var hdrs : [ String : String ] = [ : ] ; for l in lines. dropFirst ( ) { let kv = l. components ( separatedBy: " : " ) ; if kv. count >= 2 { hdrs [ kv [ 0 ] . lowercased ( ) ] = kv. dropFirst ( ) . joined ( separator: " : " ) } }
161156 let rawBody = parts. dropFirst ( ) . joined ( separator: " \r \n \r \n " )
162157 if let cl = hdrs [ " content-length " ] , let n = Int ( cl) , rawBody. utf8. count < n { return nil }
163158 method = tokens [ 0 ] ; path = tokens [ 1 ] . components ( separatedBy: " ? " ) . first ?? tokens [ 1 ] ; body = rawBody
0 commit comments