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
|
|
@ -12,7 +12,6 @@ axum.workspace = true
|
|||
base64.workspace = true
|
||||
reqwest.workspace = true
|
||||
clap.workspace = true
|
||||
hive-fr0nt.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
libc = "0.2"
|
||||
rusqlite.workspace = true
|
||||
|
|
@ -20,6 +19,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
tower-http.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,116 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>hyperhive // h1ve-c0re</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre class="banner">
|
||||
░▒▓█▓▒░ HYPERHIVE ░▒▓█▓▒░ HIVE-C0RE ░▒▓█▓▒░ WE ARE THE WIRED ░▒▓█▓▒░
|
||||
</pre>
|
||||
|
||||
<div id="notif-row" class="notif-row">
|
||||
<button type="button" id="notif-enable" class="btn btn-notif" hidden>🔔 enable notifications</button>
|
||||
<button type="button" id="notif-mute" class="btn btn-notif" hidden>🔕 mute</button>
|
||||
<button type="button" id="notif-unmute" class="btn btn-notif" hidden>🔔 unmute</button>
|
||||
<span id="notif-status" class="meta" hidden></span>
|
||||
</div>
|
||||
|
||||
<!-- swarm: live containers, dormant state, meta input bumps that
|
||||
affect the whole swarm. -->
|
||||
<h2>◆ C0NTAINERS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="containers-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ K3PT ST4T3 ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="tombstones-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ M3T4 1NPUTS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p class="meta">select inputs to <code>nix flake update</code> in <code>/meta/</code>. selected agents rebuild in sequence after the lock bump; manager learns each outcome via the usual <code>rebuilt</code> system event.</p>
|
||||
<div id="meta-inputs-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ R3BU1LD QU3U3 ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p class="meta">pending + running rebuilds, meta-updates, and first-spawns. one runs at a time; meta-update cascades nest under their parent. dedup: re-enqueueing a still-queued op collapses into the existing entry.</p>
|
||||
<div id="rebuild-queue-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<!-- operator decisions: things waiting on you. -->
|
||||
<h2>◆ M1ND H4S QU3STI0NS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="questions-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ QU3U3D R3M1ND3RS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p class="meta">reminders agents have queued for themselves but not yet delivered. cancel to drop a stuck or unwanted entry.</p>
|
||||
<div id="reminders-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ P3NDING APPR0VALS ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="approvals-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<!-- messages: broker traffic + the compose box that produces it. -->
|
||||
<h2>◆ 0PER4T0R 1NB0X ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<div id="inbox-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h2>◆ MESS4GE FL0W ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p class="meta">live tail — newest at the top. tap on every <code>send</code> / <code>recv</code> through the broker. compose below: <code>@name</code> picks the recipient (sticky until you @ someone else); <code>tab</code> completes.</p>
|
||||
<div class="terminal-wrap">
|
||||
<div id="msgflow" class="live"><div class="meta">connecting…</div></div>
|
||||
<div id="op-compose" class="op-compose">
|
||||
<span id="op-compose-prompt" class="op-compose-prompt">@—></span>
|
||||
<textarea id="op-compose-input" class="op-compose-input"
|
||||
placeholder="@agent message… (enter sends, shift+enter newline, tab completes @-mention)"
|
||||
rows="1" autocomplete="off"></textarea>
|
||||
<div id="op-compose-suggest" class="op-compose-suggest" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p>▲△▲ <a href="https://git.berlin.ccc.de/vinzenz/hyperhive">hyperhive</a> ▲△▲ hive-c0re on this host ▲△▲</p>
|
||||
</footer>
|
||||
|
||||
<!-- Slide-in detail panel. Long content (clicked file previews,
|
||||
approval diffs, journald logs, applied config) opens here
|
||||
instead of expanding inline. Singleton — JS swaps the title +
|
||||
body and toggles `.open`. -->
|
||||
<div id="side-panel" class="side-panel" aria-hidden="true">
|
||||
<div class="side-panel-backdrop" id="side-panel-backdrop"></div>
|
||||
<aside class="side-panel-drawer" role="dialog" aria-modal="true"
|
||||
aria-labelledby="side-panel-title">
|
||||
<header class="side-panel-head">
|
||||
<span class="side-panel-title" id="side-panel-title"></span>
|
||||
<button type="button" class="side-panel-close" id="side-panel-close"
|
||||
title="close (esc)">✕</button>
|
||||
</header>
|
||||
<div class="side-panel-body" id="side-panel-body"></div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script src="/static/hive-fr0nt.js" defer></script>
|
||||
<script src="/static/marked.js" defer></script>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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