Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 94633e0

Browse files
z23ccclaude
andcommitted
feat(frontend): add toast notifications, responsive layout, keyboard shortcuts, and breadcrumbs
- Add sonner Toaster + WebSocket toast bridge (TaskCompleted/Failed/EpicCompleted) - Responsive sidebar with hamburger menu (mobile) and overlay - Keyboard shortcuts: D(dashboard), G(DAG), A(agents), M(memory), S(settings) - Enhanced breadcrumbs with route-aware links and epic context - Responsive task table with stacked layout on small screens - Shortcut hints displayed as kbd badges next to nav items Task: fn-15-react-vite-shadcnui-react-flow-leptos.7 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ec7f8d3 commit 94633e0

12,841 files changed

Lines changed: 1982456 additions & 1260 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

flowctl/Cargo.lock

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

flowctl/Cargo.toml

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ members = [
77
"crates/flowctl-service",
88
"crates/flowctl-cli",
99
"crates/flowctl-daemon",
10-
"crates/flowctl-web",
1110
]
1211

1312
# ── Shared package metadata (nushell convention) ──────────────────────
@@ -86,24 +85,3 @@ codegen-units = 1
8685
panic = "abort"
8786
strip = true
8887

89-
# ── WASM release profile ─────────────────────────────────────────────
90-
[profile.wasm-release]
91-
inherits = "release"
92-
opt-level = "z"
93-
lto = "fat"
94-
codegen-units = 1
95-
96-
# ── Leptos web frontend configuration ────────────────────────────────
97-
[[workspace.metadata.leptos]]
98-
name = "flowctl-web"
99-
bin-package = "flowctl-cli"
100-
lib-package = "flowctl-web"
101-
site-root = "target/site"
102-
site-pkg-dir = "pkg"
103-
style-file = "crates/flowctl-web/style/main.css"
104-
assets-dir = "crates/flowctl-web/public"
105-
site-addr = "127.0.0.1:17319"
106-
reload-port = 17320
107-
bin-features = ["daemon"]
108-
lib-features = ["hydrate"]
109-
lib-profile-release = "wasm-release"

flowctl/crates/flowctl-cli/Cargo.toml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ path = "src/main.rs"
1212

1313
[features]
1414
default = ["daemon"]
15-
daemon = ["dep:flowctl-daemon", "dep:flowctl-web", "dep:tokio", "dep:leptos", "dep:leptos_axum", "dep:any_spawner", "dep:axum"]
15+
daemon = ["dep:flowctl-daemon", "dep:tokio", "dep:axum", "dep:tower-http"]
1616

1717
[dependencies]
1818
flowctl-core = { workspace = true }
@@ -21,12 +21,8 @@ flowctl-service = { workspace = true }
2121
rusqlite = { workspace = true }
2222
flowctl-scheduler = { workspace = true }
2323
flowctl-daemon = { path = "../flowctl-daemon", features = ["daemon"], optional = true }
24-
flowctl-web = { path = "../flowctl-web", features = ["ssr"], optional = true }
25-
26-
leptos = { version = "0.8", features = ["ssr"], optional = true }
27-
leptos_axum = { version = "0.8", optional = true }
28-
any_spawner = { version = "0.3", optional = true }
2924
axum = { workspace = true, optional = true }
25+
tower-http = { version = "0.6", features = ["fs"], optional = true }
3026
serde = { workspace = true }
3127
serde_json = { workspace = true }
3228
anyhow = { workspace = true }

flowctl/crates/flowctl-cli/src/main.rs

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -613,65 +613,35 @@ fn main() {
613613
let result = if let Some(tcp_port) = port {
614614
println!("flowctl daemon starting on http://127.0.0.1:{tcp_port}");
615615

616-
// Initialize the Leptos/tokio executor for SSR rendering.
617-
let _ = any_spawner::Executor::init_tokio();
618-
619616
// Build API router from daemon.
620617
let (state, cancel) = flowctl_daemon::server::create_state(runtime, event_bus)
621618
.expect("failed to create state");
622619
let api_router = flowctl_daemon::server::build_router(state);
623620

624-
// Serve static CSS at /pkg/flowctl-web.css
625-
let css_content = include_str!("../../flowctl-web/style/main.css");
626-
let css_content_owned = css_content.to_string();
627-
let css_handler = axum::routing::get(move || {
628-
let css = css_content_owned.clone();
629-
async move {
630-
(
631-
[(axum::http::header::CONTENT_TYPE, "text/css")],
632-
css,
633-
)
634-
}
635-
});
636-
637-
// Add Leptos SSR fallback: API routes take priority,
638-
// everything else renders the Leptos app via SSR.
639-
// Wrap in HTML shell since we're not using leptos_meta.
640-
let ssr_handler = leptos_axum::render_app_to_stream(
641-
flowctl_web::app::App,
642-
);
643-
let shell_handler = move |req: axum::http::Request<axum::body::Body>| {
644-
let ssr = ssr_handler.clone();
645-
async move {
646-
let resp = ssr(req).await;
647-
let (parts, body) = resp.into_parts();
648-
let bytes = axum::body::to_bytes(body, 1024 * 1024).await.unwrap_or_default();
649-
let inner_html = String::from_utf8_lossy(&bytes);
650-
let full_html = format!(
651-
r#"<!DOCTYPE html>
652-
<html lang="en" class="dark">
653-
<head>
654-
<meta charset="utf-8"/>
655-
<meta name="viewport" content="width=device-width, initial-scale=1"/>
656-
<title>flowctl — AI Development Platform</title>
657-
<link rel="stylesheet" href="/pkg/flowctl-web.css"/>
658-
</head>
659-
<body class="bg-gray-900 text-gray-100 min-h-screen">
660-
{inner_html}
661-
</body>
662-
</html>"#
663-
);
664-
axum::http::Response::from_parts(
665-
parts,
666-
axum::body::Body::from(full_html),
621+
// Serve static frontend from frontend/dist/ if it exists,
622+
// with SPA fallback to index.html for non-API routes.
623+
use axum::response::IntoResponse;
624+
let dist_dir = cwd.join("frontend").join("dist");
625+
let router = if dist_dir.exists() {
626+
let index_path = dist_dir.join("index.html");
627+
let spa_fallback = axum::routing::get(move || {
628+
let path = index_path.clone();
629+
async move {
630+
match tokio::fs::read_to_string(&path).await {
631+
Ok(html) => axum::response::Html(html).into_response(),
632+
Err(_) => axum::http::StatusCode::NOT_FOUND.into_response(),
633+
}
634+
}
635+
});
636+
api_router
637+
.fallback_service(
638+
tower_http::services::ServeDir::new(&dist_dir)
639+
.not_found_service(spa_fallback),
667640
)
668-
}
641+
} else {
642+
api_router
669643
};
670644

671-
let router = api_router
672-
.route("/pkg/flowctl-web.css", css_handler)
673-
.fallback(shell_handler);
674-
675645
let addr = format!("127.0.0.1:{tcp_port}");
676646
let listener = tokio::net::TcpListener::bind(&addr).await
677647
.expect("failed to bind TCP");

flowctl/crates/flowctl-daemon/src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub fn create_state(runtime: DaemonRuntime, event_bus: flowctl_scheduler::EventB
3636
}
3737

3838
/// Build the Axum router with all daemon API routes.
39-
/// Public so the CLI can merge this with other routes (e.g. Leptos SSR).
39+
/// Public so the CLI can merge this with other routes (e.g. static file serving).
4040
pub fn build_router(state: AppState) -> axum::Router {
4141
let cors = CorsLayer::new()
4242
.allow_origin(Any)

frontend/dist/assets/index-DaNm_tyw.js

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

frontend/dist/assets/index-DvONfYNF.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/dist/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en" class="dark">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Flow Code</title>
7+
<script type="module" crossorigin src="/assets/index-DaNm_tyw.js"></script>
8+
<link rel="stylesheet" crossorigin href="/assets/index-DvONfYNF.css">
9+
</head>
10+
<body class="bg-bg-primary text-text-primary min-h-screen antialiased">
11+
<div id="root"></div>
12+
</body>
13+
</html>

frontend/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en" class="dark">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Flow Code</title>
7+
</head>
8+
<body class="bg-bg-primary text-text-primary min-h-screen antialiased">
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

frontend/node_modules/.bin/jiti

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

0 commit comments

Comments
 (0)