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
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
|
@ -14,7 +14,7 @@ use axum::{
|
|||
extract::{Path as AxumPath, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{
|
||||
Html, IntoResponse, Response,
|
||||
IntoResponse, Response,
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
},
|
||||
routing::{get, post},
|
||||
|
|
@ -23,6 +23,7 @@ use hive_sh4re::Approval;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
use tokio_stream::{Stream, StreamExt};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::actions;
|
||||
use crate::container_view::{ContainerView, claude_has_session};
|
||||
|
|
@ -37,11 +38,20 @@ struct AppState {
|
|||
}
|
||||
|
||||
pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||
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 bundled \
|
||||
dashboard dist (see services.hive-c0re.frontend 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(), "dashboard static dir resolved");
|
||||
let app = Router::new()
|
||||
.route("/", get(serve_index))
|
||||
.route("/static/dashboard.css", get(serve_css))
|
||||
.route("/static/app.js", get(serve_app_js))
|
||||
.route("/favicon.svg", get(serve_favicon))
|
||||
.route("/api/state", get(api_state))
|
||||
.route("/approve/{id}", post(post_approve))
|
||||
.route("/deny/{id}", post(post_deny))
|
||||
|
|
@ -66,8 +76,11 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/dashboard/stream", get(dashboard_stream))
|
||||
.route("/dashboard/history", get(dashboard_history))
|
||||
.route("/static/hive-fr0nt.js", get(serve_shared_js))
|
||||
.route("/static/marked.js", get(serve_marked_js))
|
||||
// Anything not matched by the dynamic routes above falls
|
||||
// through to the bundled dashboard dist (GET / →
|
||||
// dist/index.html, /favicon.svg → dist/favicon.svg,
|
||||
// /static/dashboard.css → dist/static/dashboard.css, etc.).
|
||||
.fallback_service(ServeDir::new(&static_dir))
|
||||
.with_state(AppState { coord });
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let listener = bind_with_retry(addr).await?;
|
||||
|
|
@ -77,11 +90,14 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static asset handlers: the dashboard is an SPA. `GET /` returns the
|
||||
// (static) shell; `GET /static/*` serves the CSS + JS app; `GET /api/state`
|
||||
// returns the current snapshot as JSON. The JS app fetches state on load,
|
||||
// re-fetches after every async-form submit, and listens on
|
||||
// `/dashboard/stream` for the unified live event channel.
|
||||
// The dashboard is an SPA. Its HTML shell + bundled JS / CSS / favicon
|
||||
// live in the directory pointed at by `HIVE_STATIC_DIR` (set by the
|
||||
// hive-c0re NixOS module to `${frontend}/dashboard`), served by the
|
||||
// `tower_http::ServeDir` fallback declared in `serve()`. The dynamic
|
||||
// surface — `/api/state` and the action endpoints — is owned here.
|
||||
// The JS app fetches state on load, re-fetches after every async-form
|
||||
// submit, and listens on `/dashboard/stream` for the unified live event
|
||||
// channel.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `SO_REUSEADDR` bind with retry. Mirrors the per-agent variant in
|
||||
|
|
@ -142,56 +158,6 @@ fn try_bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
|
|||
sock.listen(1024)
|
||||
}
|
||||
|
||||
async fn serve_index() -> impl IntoResponse {
|
||||
Html(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/dashboard.css"),
|
||||
);
|
||||
([("content-type", "text/css")], body)
|
||||
}
|
||||
|
||||
async fn serve_app_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
include_str!("../assets/app.js"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Dashboard favicon — the hyperhive mark. Static: the dashboard
|
||||
/// represents the whole hive, so it always uses the project logo
|
||||
/// (per-agent pages serve their own configurable `/icon` instead).
|
||||
async fn serve_favicon() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "image/svg+xml")],
|
||||
include_str!("../../branding/hyperhive.svg"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_shared_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
hive_fr0nt::TERMINAL_JS,
|
||||
)
|
||||
}
|
||||
|
||||
/// Vendored `marked` bundle — the side panel renders markdown file
|
||||
/// previews with it.
|
||||
async fn serve_marked_js() -> impl IntoResponse {
|
||||
(
|
||||
[("content-type", "application/javascript")],
|
||||
hive_fr0nt::MARKED_JS,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StateSnapshot {
|
||||
/// Broker seq at the moment this snapshot was assembled. Clients
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue