Compare commits
4 changed files with 2 additions and 44 deletions
|
|
@ -358,10 +358,6 @@ Layout, top to bottom:
|
||||||
|
|
||||||
- Banner (gradient shimmer while state=thinking).
|
- Banner (gradient shimmer while state=thinking).
|
||||||
- Title with `↑ DASHB04RD` back-link (new tab) + `↻ R3BU1LD`.
|
- Title with `↑ DASHB04RD` back-link (new tab) + `↻ R3BU1LD`.
|
||||||
- Meta links row: `📊 stats →`, `🖥 screen →` (shown when
|
|
||||||
`gui_enabled`), and `⬡ forge ↗` (shown when `/api/state`
|
|
||||||
supplies a non-null `forge_url` — the agent's Forgejo profile,
|
|
||||||
assembled server-side from the request `Host` header, new tab).
|
|
||||||
- Status section: empty when online (alive-badge in the state
|
- Status section: empty when online (alive-badge in the state
|
||||||
row carries the signal), populated with the login form /
|
row carries the signal), populated with the login form /
|
||||||
OAuth URL when `status` is `needs_login_*`.
|
OAuth URL when `status` is `needs_login_*`.
|
||||||
|
|
|
||||||
|
|
@ -677,17 +677,6 @@
|
||||||
// Show the screen link when the weston VNC compositor is enabled.
|
// Show the screen link when the weston VNC compositor is enabled.
|
||||||
const screenLink = $('screen-link');
|
const screenLink = $('screen-link');
|
||||||
if (screenLink) screenLink.style.display = s.gui_enabled ? '' : 'none';
|
if (screenLink) screenLink.style.display = s.gui_enabled ? '' : 'none';
|
||||||
// Forge profile link — the backend hands us the full URL (or
|
|
||||||
// null when this agent has no forge account).
|
|
||||||
const forgeLink = $('forge-link');
|
|
||||||
if (forgeLink) {
|
|
||||||
if (s.forge_url) {
|
|
||||||
forgeLink.href = s.forge_url;
|
|
||||||
forgeLink.style.display = '';
|
|
||||||
} else {
|
|
||||||
forgeLink.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderTermInput(s.label, s.status === 'online');
|
renderTermInput(s.label, s.status === 'online');
|
||||||
renderInbox(s.inbox || []);
|
renderInbox(s.inbox || []);
|
||||||
// Authoritative state comes from the harness via /api/state.
|
// Authoritative state comes from the harness via /api/state.
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,6 @@
|
||||||
<a href="/stats" style="color: var(--cyan); text-decoration: none;">📊 stats →</a>
|
<a href="/stats" style="color: var(--cyan); text-decoration: none;">📊 stats →</a>
|
||||||
<a id="screen-link" href="/screen" target="_blank" rel="noopener"
|
<a id="screen-link" href="/screen" target="_blank" rel="noopener"
|
||||||
style="display:none; color: var(--cyan); text-decoration: none; margin-left: 1em;">🖥 screen →</a>
|
style="display:none; color: var(--cyan); text-decoration: none; margin-left: 1em;">🖥 screen →</a>
|
||||||
<a id="forge-link" href="#" target="_blank" rel="noopener"
|
|
||||||
title="this agent's profile on the hive forge"
|
|
||||||
style="display:none; color: var(--cyan); text-decoration: none; margin-left: 1em;">⬡ forge ↗</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="status">
|
<div id="status">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use anyhow::{Context, Result};
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, Router,
|
Form, Router,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{HeaderMap, StatusCode},
|
http::StatusCode,
|
||||||
response::{
|
response::{
|
||||||
IntoResponse, Response,
|
IntoResponse, Response,
|
||||||
sse::{Event, KeepAlive, Sse},
|
sse::{Event, KeepAlive, Sse},
|
||||||
|
|
@ -383,11 +383,6 @@ struct StateSnapshot {
|
||||||
/// (i.e. `/etc/hyperhive/gui.json` was present at harness startup).
|
/// (i.e. `/etc/hyperhive/gui.json` was present at harness startup).
|
||||||
/// When true, the UI may render a `🖥 screen` link to `/screen`.
|
/// When true, the UI may render a `🖥 screen` link to `/screen`.
|
||||||
gui_enabled: bool,
|
gui_enabled: bool,
|
||||||
/// Full URL of this agent's Forgejo profile, or `null` when the
|
|
||||||
/// agent has no forge account (`forge-token` absent). Assembled
|
|
||||||
/// server-side from the request `Host` header so the UI just
|
|
||||||
/// links it — no client-side URL construction.
|
|
||||||
forge_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -445,7 +440,7 @@ async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
||||||
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
|
||||||
// Capture seq *before* any reads so the dedupe contract is
|
// Capture seq *before* any reads so the dedupe contract is
|
||||||
// "events with seq > snapshot.seq are post-snapshot, never missed."
|
// "events with seq > snapshot.seq are post-snapshot, never missed."
|
||||||
let seq = state.bus.current_seq();
|
let seq = state.bus.current_seq();
|
||||||
|
|
@ -490,28 +485,9 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
||||||
ctx_usage,
|
ctx_usage,
|
||||||
cost_usage,
|
cost_usage,
|
||||||
gui_enabled: state.gui_vnc_port.is_some(),
|
gui_enabled: state.gui_vnc_port.is_some(),
|
||||||
forge_url: forge_profile_url(&headers, &state.label),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This agent's Forgejo profile URL — `Some` only when the agent has a
|
|
||||||
/// forge account (its `forge-token` file exists). The host is taken
|
|
||||||
/// from the request `Host` header so the link resolves against
|
|
||||||
/// whatever host the operator reached the page on; the forge always
|
|
||||||
/// listens on `:3000`, and the per-agent forge user is named after
|
|
||||||
/// the agent (`label`).
|
|
||||||
fn forge_profile_url(headers: &HeaderMap, label: &str) -> Option<String> {
|
|
||||||
if !crate::paths::state_dir().join("forge-token").is_file() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let host = headers
|
|
||||||
.get("host")
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.unwrap_or("localhost");
|
|
||||||
let hostname = host.split(':').next().unwrap_or(host);
|
|
||||||
Some(format!("http://{hostname}:3000/{label}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Best-effort: pull the last 30 messages addressed to us via the
|
/// Best-effort: pull the last 30 messages addressed to us via the
|
||||||
/// per-agent / manager socket. Empty list on any transport / decode
|
/// per-agent / manager socket. Empty list on any transport / decode
|
||||||
/// failure — the inbox section is decorative, not authoritative.
|
/// failure — the inbox section is decorative, not authoritative.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue