frontend: cut over Rust binaries to ServeDir; delete legacy assets
Phase 4 of #273 — the actual switch. Both axum routers now serve their static surface via `tower_http::services::ServeDir` mounted as a fallback service, reading the dist path from `HIVE_STATIC_DIR` (set by Phase 3's NixOS module wiring). Deletes: - `hive-c0re/assets/{index.html, app.js, dashboard.css}` - `hive-ag3nt/assets/{index.html, app.js, agent.css, stats.html, stats.js, screen.html}` - The whole `hive-fr0nt/` crate (workspace member dropped, both hive-c0re and hive-ag3nt drop their `hive-fr0nt.workspace = true` dep). Its contents now live as `@hive/shared` under `frontend/packages/shared/`. Rust changes: - `hive-c0re/src/dashboard.rs`: remove `serve_index`, `serve_css`, `serve_app_js`, `serve_shared_js`, `serve_marked_js`, `serve_favicon` (all six `include_str!` handlers); replace their routes with a single `.fallback_service(ServeDir::new(static_dir))` on the router. Fail closed (anyhow::bail) if `HIVE_STATIC_DIR` is unset or not a directory at startup. - `hive-ag3nt/src/web_ui.rs`: remove `serve_index`, `serve_css`, `serve_app_js`, `serve_shared_js`, `serve_marked_js`, `serve_stats`, `serve_stats_js`, `serve_screen`; same `fallback_service` pattern. `serve_icon` stays (consumes `/etc/hyperhive/icon.svg` + `branding/hyperhive.svg` fallback, neither of which lives under the frontend dist). - `AgentLink` URLs for stats/screen switched from `/stats` / `/screen` to `/stats.html` / `/screen.html` since ServeDir doesn't auto- append the extension and the on-disk filename is the natural URL post-cutover. - `Cargo.toml` (workspace): drop `hive-fr0nt` member + workspace dep, add `tower-http = { version = "0.6", features = ["fs"] }`. - `hive-c0re/Cargo.toml` + `hive-ag3nt/Cargo.toml`: drop the `hive-fr0nt.workspace = true` dep, add `tower-http.workspace = true`. Docs updated: - `CLAUDE.md`: file map reflects `frontend/` (was `hive-fr0nt/` + `assets/`) and the ServeDir/HIVE_STATIC_DIR shape. - `docs/web-ui.md` 'Shape (shared by both)' section: describes the ServeDir fallback + bundled-by-esbuild surface, no more `include_str!` references. - `docs/terminal-rendering.md`: src paths point at `frontend/packages/{agent,shared}/src/`; marked is the npm dep, not vendored UMD. Validation: - `cargo check --workspace` — clean (5 warnings, all pre-existing in `rebuild_queue.rs`, none on changed files). - `cargo clippy --workspace --all-targets` — clean (11 warnings, same pre-existing source). - `cd frontend && npm run build` from the prior commit's lockfile produces the dist directories the new routers consume: dashboard: `dist/{index.html, static/{app.js, dashboard.css}}` agent: `dist/{index.html, stats.html, screen.html, static/{app.js, stats.js, agent.css}}` (favicon.svg lands in dashboard/ during the nix build — `nix/frontend.nix` install phase copies `branding/hyperhive.svg` there, since it's outside the npm tree.) Refs #273.
This commit is contained in:
parent
2ecf15bb6f
commit
229c4292e9
24 changed files with 143 additions and 10122 deletions
|
|
@ -24,6 +24,7 @@ use axum::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_stream::{Stream, StreamExt, wrappers::BroadcastStream};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::client;
|
||||
use crate::events::Bus;
|
||||
|
|
@ -88,6 +89,19 @@ pub async fn serve(
|
|||
turn_lock: TurnLock,
|
||||
) -> Result<()> {
|
||||
let gui_vnc_port = read_gui_json();
|
||||
let static_dir: PathBuf = std::env::var_os("HIVE_STATIC_DIR")
|
||||
.map(PathBuf::from)
|
||||
.context(
|
||||
"HIVE_STATIC_DIR env var not set — point it at the merged \
|
||||
per-agent dist (see hyperhive.frontend.mergedDist in nix)",
|
||||
)?;
|
||||
if !static_dir.is_dir() {
|
||||
anyhow::bail!(
|
||||
"HIVE_STATIC_DIR ({}) is not a directory",
|
||||
static_dir.display()
|
||||
);
|
||||
}
|
||||
tracing::info!(static_dir = %static_dir.display(), "web UI static dir resolved");
|
||||
let state = AppState {
|
||||
label,
|
||||
login,
|
||||
|
|
@ -99,11 +113,6 @@ pub async fn serve(
|
|||
gui_vnc_port,
|
||||
};
|
||||
let app = Router::new()
|
||||
.route("/", get(serve_index))
|
||||
.route("/static/agent.css", get(serve_css))
|
||||
.route("/static/app.js", get(serve_app_js))
|
||||
.route("/static/hive-fr0nt.js", get(serve_shared_js))
|
||||
.route("/static/marked.js", get(serve_marked_js))
|
||||
.route("/api/state", get(api_state))
|
||||
.route("/events/stream", get(events_stream))
|
||||
.route("/events/history", get(events_history))
|
||||
|
|
@ -116,12 +125,17 @@ pub async fn serve(
|
|||
.route("/api/model", post(post_set_model))
|
||||
.route("/api/new-session", post(post_new_session))
|
||||
.route("/api/loose-ends", get(api_loose_ends))
|
||||
.route("/stats", get(serve_stats))
|
||||
.route("/static/stats.js", get(serve_stats_js))
|
||||
.route("/api/stats", get(api_stats))
|
||||
.route("/screen", get(serve_screen))
|
||||
.route("/screen/ws", get(screen_ws))
|
||||
.route("/icon", get(serve_icon))
|
||||
// Anything else (`/`, `/stats`, `/screen`, `/static/*`)
|
||||
// falls through to the merged dist. ServeDir auto-appends
|
||||
// `.html` when the URL is a bare path that matches a file
|
||||
// (so `/stats` → `dist/stats.html`, `/screen` → `dist/
|
||||
// screen.html`). Per-agent `extraFiles` additions are
|
||||
// already layered into this same directory (see
|
||||
// hyperhive.frontend.mergedDist in nix).
|
||||
.fallback_service(ServeDir::new(&static_dir))
|
||||
.with_state(state);
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = bind_with_retry(addr, "web UI").await?;
|
||||
|
|
@ -201,68 +215,6 @@ fn try_bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
|
|||
sock.listen(1024)
|
||||
}
|
||||
|
||||
async fn serve_index() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/html; charset=utf-8")],
|
||||
include_str!("../assets/index.html"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_css() -> impl IntoResponse {
|
||||
// Prepend the shared palette/typography so per-page styles only need
|
||||
// to declare what's actually page-specific. One HTTP request, no
|
||||
// per-asset cache to invalidate.
|
||||
let body = format!(
|
||||
"{}\n{}\n{}",
|
||||
hive_fr0nt::BASE_CSS,
|
||||
hive_fr0nt::TERMINAL_CSS,
|
||||
include_str!("../assets/agent.css"),
|
||||
);
|
||||
([("content-type", "text/css")], body)
|
||||
}
|
||||
|
||||
async fn serve_app_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
include_str!("../assets/app.js"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_shared_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
hive_fr0nt::TERMINAL_JS,
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_marked_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
hive_fr0nt::MARKED_JS,
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_stats() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/html; charset=utf-8")],
|
||||
include_str!("../assets/stats.html"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_stats_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
include_str!("../assets/stats.js"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_screen() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "text/html; charset=utf-8")],
|
||||
include_str!("../assets/screen.html"),
|
||||
)
|
||||
}
|
||||
|
||||
/// This agent's icon. Serves the operator-configured SVG from
|
||||
/// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix
|
||||
/// option) when present, otherwise the bundled default hyperhive logo.
|
||||
|
|
@ -585,8 +537,13 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
|||
fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
||||
let mut links = Vec::new();
|
||||
|
||||
// Note: the URLs are the actual HTML files served out of the
|
||||
// frontend dist (`stats.html` / `screen.html`); after the #273
|
||||
// backend/frontend split the harness serves these as static
|
||||
// files via ServeDir rather than via Rust routes, so the URL
|
||||
// has to be the on-disk filename.
|
||||
links.push(AgentLink {
|
||||
url: "/stats".to_owned(),
|
||||
url: "/stats.html".to_owned(),
|
||||
icon: "📊".to_owned(),
|
||||
label: "stats".to_owned(),
|
||||
kind: AgentLinkKind::Container,
|
||||
|
|
@ -594,7 +551,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
|||
|
||||
if gui_enabled {
|
||||
links.push(AgentLink {
|
||||
url: "/screen".to_owned(),
|
||||
url: "/screen.html".to_owned(),
|
||||
icon: "🖥".to_owned(),
|
||||
label: "screen".to_owned(),
|
||||
kind: AgentLinkKind::Container,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue