Skip to content

Commit f5b81dc

Browse files
committed
Embed GUI into dwata-api and add build workflow
1 parent 2ec88cc commit f5b81dc

7 files changed

Lines changed: 112 additions & 19 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: build-release
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: "20"
20+
cache: npm
21+
cache-dependency-path: gui/package-lock.json
22+
23+
- name: Setup Rust
24+
uses: dtolnay/rust-toolchain@stable
25+
26+
- name: Build production artifacts
27+
run: ./scripts/build-production.sh
28+
29+
- name: Upload dwata-api binary
30+
uses: actions/upload-artifact@v4
31+
with:
32+
name: dwata-api-linux-x64
33+
path: target/release/dwata-api

docs/05-api-reference.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub struct AgentToolCall {
6868

6969
Check API and database connectivity.
7070

71-
**Endpoint**: `GET /health`
71+
**Endpoint**: `GET /api/health`
7272

7373
**Response**:
7474
```json
@@ -88,16 +88,7 @@ Check API and database connectivity.
8888

8989
### Root
9090

91-
Basic API information.
92-
93-
**Endpoint**: `GET /`
94-
95-
**Response**:
96-
```json
97-
{
98-
"message": "Hello World"
99-
}
100-
```
91+
The root path serves the GUI in production builds. For a basic API response, use `GET /api/hello`.
10192

10293
## Session Management
10394

dwata-api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ regex = "1.10"
4040
futures = "0.3"
4141
r2d2 = "0.8"
4242
r2d2_sqlite = "0.25"
43+
mime_guess = "2.0"
44+
rust-embed = "8.5"
45+
webbrowser = "0.8.15"

dwata-api/src/main.rs

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use actix_cors::Cors;
2-
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
2+
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
33
use clap::Parser;
4+
use rust_embed::RustEmbed;
45
use std::sync::Arc;
56
use tracing_subscriber::prelude::*;
67

@@ -12,14 +13,43 @@ mod integrations;
1213
mod jobs;
1314
mod financial_keywords;
1415

15-
#[get("/")]
16+
#[derive(RustEmbed)]
17+
#[folder = "../gui/dist"]
18+
struct GuiAssets;
19+
20+
fn gui_response_for_path(path: &str) -> HttpResponse {
21+
if let Some(content) = GuiAssets::get(path) {
22+
let mime = mime_guess::from_path(path).first_or_octet_stream();
23+
HttpResponse::Ok()
24+
.content_type(mime.as_ref())
25+
.body(content.data.into_owned())
26+
} else {
27+
match GuiAssets::get("index.html") {
28+
Some(index) => HttpResponse::Ok()
29+
.content_type("text/html; charset=utf-8")
30+
.body(index.data.into_owned()),
31+
None => HttpResponse::InternalServerError().body("GUI assets not found"),
32+
}
33+
}
34+
}
35+
36+
async fn serve_gui(req: HttpRequest) -> HttpResponse {
37+
let path = req.path().trim_start_matches('/');
38+
if path == "api" || path.starts_with("api/") {
39+
return HttpResponse::NotFound().finish();
40+
}
41+
let path = if path.is_empty() { "index.html" } else { path };
42+
gui_response_for_path(path)
43+
}
44+
45+
#[get("/api/hello")]
1646
async fn hello() -> impl Responder {
1747
HttpResponse::Ok().json(serde_json::json!({
1848
"message": "Hello World"
1949
}))
2050
}
2151

22-
#[get("/health")]
52+
#[get("/api/health")]
2353
async fn health(db: web::Data<Arc<database::Database>>) -> impl Responder {
2454
// Test database connection
2555
match db.connection.lock() {
@@ -34,12 +64,12 @@ async fn health(db: web::Data<Arc<database::Database>>) -> impl Responder {
3464
}
3565
}
3666

37-
#[get("/settings")]
67+
#[get("/api/settings")]
3868
async fn get_settings(data: web::Data<handlers::settings::SettingsAppState>) -> impl Responder {
3969
handlers::settings::get_settings(data).await
4070
}
4171

42-
#[post("/settings/api-keys")]
72+
#[post("/api/settings/api-keys")]
4373
async fn update_api_keys(
4474
data: web::Data<handlers::settings::SettingsAppState>,
4575
request: web::Json<shared_types::UpdateApiKeysRequest>,
@@ -53,6 +83,8 @@ async fn update_api_keys(
5383
struct Args {
5484
#[arg(long)]
5585
log_file_path: Option<String>,
86+
#[arg(long)]
87+
no_open: bool,
5688
}
5789

5890
#[actix_web::main]
@@ -393,13 +425,25 @@ async fn main() -> std::io::Result<()> {
393425
.route("/api/financial/patterns/{id}/toggle", web::patch().to(handlers::financial::toggle_pattern))
394426
.service(handlers::pattern_generation::process_sender)
395427
.service(handlers::pattern_generation::generate_pattern)
428+
.default_service(web::route().to(serve_gui))
396429
})
397430
.bind((host.as_str(), port))?
398431
.run();
399432

400433
let handle = server.handle();
401434
let shutdown_manager = download_manager.clone();
402435

436+
let open_in_browser = !args.no_open && std::env::var("DWATA_NO_OPEN").is_err();
437+
if open_in_browser {
438+
let url = format!("http://{}:{}/", host, port);
439+
tokio::spawn(async move {
440+
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
441+
if let Err(err) = webbrowser::open(&url) {
442+
tracing::warn!("Failed to open browser: {}", err);
443+
}
444+
});
445+
}
446+
403447
tokio::spawn(async move {
404448
if let Err(e) = tokio::signal::ctrl_c().await {
405449
tracing::error!("Failed to listen for Ctrl+C: {}", e);

gui/src/config/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
const API_HOST = import.meta.env.VITE_API_HOST || "127.0.0.1";
77
const API_PORT = import.meta.env.VITE_API_PORT || "9200";
88

9-
export const API_BASE_URL = `http://${API_HOST}:${API_PORT}`;
9+
const DEFAULT_DEV_API_BASE_URL = `http://${API_HOST}:${API_PORT}`;
10+
const DEFAULT_PROD_API_BASE_URL =
11+
typeof window !== "undefined" ? window.location.origin : DEFAULT_DEV_API_BASE_URL;
12+
13+
export const API_BASE_URL =
14+
import.meta.env.VITE_API_BASE_URL ||
15+
(import.meta.env.DEV ? DEFAULT_DEV_API_BASE_URL : DEFAULT_PROD_API_BASE_URL);
1016

1117
/**
1218
* Helper function to build API URLs

gui/src/pages/settings/ApiKeys.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function SettingsApiKeys() {
2525

2626
const fetchSettings = async () => {
2727
try {
28-
const response = await fetch(getApiUrl("/settings"));
28+
const response = await fetch(getApiUrl("/api/settings"));
2929
if (response.ok) {
3030
const data: SettingsResponse = await response.json();
3131
setApiKeys(data.api_keys);
@@ -39,7 +39,7 @@ export default function SettingsApiKeys() {
3939
setIsLoading(true);
4040
setMessage("");
4141
try {
42-
const response = await fetch(getApiUrl("/settings/api-keys"), {
42+
const response = await fetch(getApiUrl("/api/settings/api-keys"), {
4343
method: "POST",
4444
headers: {
4545
"Content-Type": "application/json",

scripts/build-production.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
6+
echo "==> Generating API types"
7+
(cd "$ROOT/shared-types" && cargo run --bin generate_api_types)
8+
9+
echo "==> Building GUI (Vite)"
10+
(cd "$ROOT/gui" && npm ci && npm run build)
11+
12+
echo "==> Building dwata-api (release)"
13+
(cd "$ROOT" && cargo build -p dwata-api --release)
14+
15+
echo "==> Done"
16+
echo "Binary: $ROOT/target/release/dwata-api"

0 commit comments

Comments
 (0)