Skip to content

Commit 3d0a039

Browse files
committed
feat(api): implement complete API structure with routing for agents, notes, tasks, and workspaces
1 parent adb49b1 commit 3d0a039

33 files changed

Lines changed: 4303 additions & 42 deletions

apps/desktop/src-tauri/Cargo.lock

Lines changed: 808 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,23 @@ tauri-plugin-os = "2"
2424
serde = { version = "1", features = ["derive"] }
2525
serde_json = "1"
2626
dirs = "5"
27+
28+
# ── Rust Backend (replacing Node.js) ──────────────────────────────
29+
axum = { version = "0.8", features = ["macros"] }
30+
tokio = { version = "1", features = ["full"] }
31+
rusqlite = { version = "0.32", features = ["bundled"] }
32+
rmcp = { version = "0.15", features = ["server", "transport-streamable-http-server", "schemars"] }
33+
schemars = "1"
34+
agent-client-protocol = "0.9"
35+
uuid = { version = "1", features = ["v4"] }
36+
chrono = { version = "0.4", features = ["serde"] }
37+
tower = "0.5"
38+
tower-http = { version = "0.6", features = ["cors", "trace"] }
39+
tracing = "0.1"
40+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
41+
thiserror = "2"
42+
glob = "0.3"
43+
tokio-stream = "0.1"
44+
45+
[dev-dependencies]
46+
reqwest = { version = "0.12", features = ["json"] }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Standalone Rust backend server (without Tauri).
2+
//! Run with: cargo run --example standalone_server
3+
4+
#[tokio::main]
5+
async fn main() {
6+
let config = routa_desktop_lib::server::ServerConfig {
7+
host: "127.0.0.1".to_string(),
8+
port: 3210,
9+
db_path: "/tmp/routa-test.db".to_string(),
10+
};
11+
12+
println!("Starting standalone Routa Rust backend on 127.0.0.1:3210...");
13+
println!("Database: /tmp/routa-test.db");
14+
println!("Press Ctrl+C to stop.\n");
15+
16+
match routa_desktop_lib::server::start_server(config).await {
17+
Ok(addr) => {
18+
println!("Server listening on http://{}", addr);
19+
println!("\nAvailable endpoints:");
20+
println!(" GET /api/health");
21+
println!(" GET /api/agents");
22+
println!(" POST /api/agents");
23+
println!(" GET /api/notes");
24+
println!(" POST /api/notes");
25+
println!(" GET /api/tasks");
26+
println!(" POST /api/tasks");
27+
println!(" GET /api/workspaces");
28+
println!(" GET /api/skills");
29+
println!(" GET /api/sessions");
30+
println!(" POST /api/acp");
31+
println!(" GET /api/notes/events (SSE)");
32+
33+
// Keep running until Ctrl+C
34+
tokio::signal::ctrl_c().await.ok();
35+
println!("\nShutting down...");
36+
}
37+
Err(e) => {
38+
eprintln!("Failed to start server: {}", e);
39+
std::process::exit(1);
40+
}
41+
}
42+
}

apps/desktop/src-tauri/src/lib.rs

Lines changed: 122 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::thread;
66
use std::time::{Duration, Instant};
77
use tauri::Manager;
88

9+
pub mod server;
10+
911
/// Custom Tauri commands exposed to the frontend via `invoke`.
1012
/// These bridge the gap between the web frontend and native capabilities.
1113
@@ -50,8 +52,6 @@ fn detect_repo_root() -> Option<PathBuf> {
5052
}
5153
}
5254

53-
// During local desktop development/build this points to:
54-
// <repo>/apps/desktop/src-tauri
5555
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
5656
let candidate = manifest_dir.join("../../..");
5757
if candidate.join("package.json").exists() {
@@ -145,8 +145,6 @@ fn start_embedded_next_server(
145145
));
146146
}
147147

148-
// Determine a writable path for the SQLite database.
149-
// Prefer ROUTA_DB_PATH env, otherwise use <app_data_dir>/routa.db.
150148
let db_path = std::env::var("ROUTA_DB_PATH").unwrap_or_else(|_| {
151149
let data_dir = app
152150
.path()
@@ -189,6 +187,51 @@ fn start_embedded_next_server(
189187
Ok(child)
190188
}
191189

190+
/// Resolve the SQLite database path for the desktop app.
191+
fn resolve_db_path(app: &tauri::AppHandle) -> String {
192+
std::env::var("ROUTA_DB_PATH").unwrap_or_else(|_| {
193+
let data_dir = app
194+
.path()
195+
.app_data_dir()
196+
.unwrap_or_else(|_| dirs::home_dir().unwrap_or_default().join(".routa"));
197+
std::fs::create_dir_all(&data_dir).ok();
198+
data_dir
199+
.join("routa.db")
200+
.to_string_lossy()
201+
.to_string()
202+
})
203+
}
204+
205+
/// Start the embedded Rust backend server (replaces Node.js).
206+
fn start_rust_server(app: &tauri::AppHandle, host: &str, port: u16) -> Result<(), String> {
207+
let db_path = resolve_db_path(app);
208+
let host = host.to_string();
209+
210+
println!("[rust-server] Starting embedded Rust backend server");
211+
println!("[rust-server] Database path: {}", db_path);
212+
println!("[rust-server] Listening on {}:{}", host, port);
213+
214+
let config = server::ServerConfig {
215+
host,
216+
port,
217+
db_path,
218+
};
219+
220+
// Start the server in the Tauri async runtime
221+
tauri::async_runtime::spawn(async move {
222+
match server::start_server(config).await {
223+
Ok(addr) => {
224+
println!("[rust-server] Server started on {}", addr);
225+
}
226+
Err(e) => {
227+
eprintln!("[rust-server] Failed to start server: {}", e);
228+
}
229+
}
230+
});
231+
232+
Ok(())
233+
}
234+
192235
#[cfg_attr(mobile, tauri::mobile_entry_point)]
193236
pub fn run() {
194237
tauri::Builder::default()
@@ -212,35 +255,67 @@ pub fn run() {
212255
let window = app.get_webview_window("main").unwrap();
213256
window.open_devtools();
214257
}
258+
259+
// Configurable API mode:
260+
// - rust (default): start embedded Rust server (no Node.js needed).
261+
// - embedded: start packaged standalone Next server (legacy Node.js mode).
262+
// - external: connect to existing server at ROUTA_DESKTOP_API_URL.
263+
// - off: use embedded static UI only.
264+
let api_mode = env_or_default("ROUTA_DESKTOP_API_MODE", "rust");
265+
let api_host = env_or_default("ROUTA_DESKTOP_API_HOST", "127.0.0.1");
266+
let port = api_port();
267+
let api_url = std::env::var("ROUTA_DESKTOP_API_URL")
268+
.unwrap_or_else(|_| format!("http://{}:{}", api_host, port));
269+
215270
#[cfg(not(debug_assertions))]
216271
{
217-
// Allow explicitly enabling DevTools in release builds for diagnostics.
218272
let force_debug =
219273
std::env::var("ROUTA_TAURI_DEBUG").ok().as_deref() == Some("1");
220274
if force_debug {
221275
if let Some(window) = app.get_webview_window("main") {
222276
window.open_devtools();
223277
}
224278
}
279+
}
225280

226-
// Configurable API mode:
227-
// - embedded (default): start packaged standalone Next server.
228-
// - external: do not spawn, connect to existing ROUTA_DESKTOP_API_URL/HOST:PORT.
229-
// - off: use embedded static UI only.
230-
let api_mode = env_or_default("ROUTA_DESKTOP_API_MODE", "embedded");
231-
let api_host = env_or_default("ROUTA_DESKTOP_API_HOST", "127.0.0.1");
232-
let port = api_port();
233-
let api_url = std::env::var("ROUTA_DESKTOP_API_URL")
234-
.unwrap_or_else(|_| format!("http://{}:{}", api_host, port));
235-
236-
if api_mode != "off" {
281+
match api_mode.as_str() {
282+
"off" => {
283+
println!("[desktop-server] API mode is off, using embedded static UI only");
284+
}
285+
"rust" => {
286+
// New: Start embedded Rust server (no Node.js dependency!)
287+
match start_rust_server(&app.handle(), &api_host, port) {
288+
Ok(()) => {
289+
// Wait for the Rust server to become ready
290+
if wait_for_port(&api_host, port, 5) {
291+
if let Some(window) = app.get_webview_window("main") {
292+
let js =
293+
format!("window.location.replace('{}');", api_url);
294+
let _ = window.eval(&js);
295+
println!(
296+
"[rust-server] Webview navigated to {}",
297+
api_url
298+
);
299+
}
300+
} else {
301+
eprintln!(
302+
"[rust-server] Timed out waiting for server. Falling back to static UI."
303+
);
304+
}
305+
}
306+
Err(e) => {
307+
eprintln!("[rust-server] {}", e);
308+
}
309+
}
310+
}
311+
"embedded" => {
312+
// Legacy: start Node.js server
237313
let mut ready = wait_for_port(&api_host, port, 1);
238-
if !ready && api_mode == "embedded" {
314+
if !ready {
239315
match start_embedded_next_server(&app.handle(), &api_host, port) {
240316
Ok(_child) => {}
241317
Err(err) => {
242318
eprintln!("[desktop-server] {}", err);
243-
// Dev fallback: running app from repository workspace.
244319
match start_local_next_server(&api_host, port) {
245320
Ok(_child) => {}
246321
Err(dev_err) => {
@@ -250,8 +325,11 @@ pub fn run() {
250325
}
251326
}
252327
ready = wait_for_port(&api_host, port, 25);
253-
} else if ready {
254-
println!("[desktop-server] Reusing existing local server on {}", api_url);
328+
} else {
329+
println!(
330+
"[desktop-server] Reusing existing local server on {}",
331+
api_url
332+
);
255333
}
256334

257335
if ready {
@@ -266,10 +344,32 @@ pub fn run() {
266344
api_url
267345
);
268346
}
269-
} else {
270-
println!("[desktop-server] API mode is off, using embedded static UI only");
347+
}
348+
"external" => {
349+
if wait_for_port(&api_host, port, 5) {
350+
if let Some(window) = app.get_webview_window("main") {
351+
let js = format!("window.location.replace('{}');", api_url);
352+
let _ = window.eval(&js);
353+
println!(
354+
"[desktop-server] Webview navigated to external {}",
355+
api_url
356+
);
357+
}
358+
} else {
359+
eprintln!(
360+
"[desktop-server] External server not reachable at {}",
361+
api_url
362+
);
363+
}
364+
}
365+
_ => {
366+
eprintln!(
367+
"[desktop-server] Unknown API mode '{}', falling back to static UI",
368+
api_mode
369+
);
271370
}
272371
}
372+
273373
Ok(())
274374
})
275375
.run(tauri::generate_context!())

0 commit comments

Comments
 (0)