diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js index 7723a754..75059976 100644 --- a/frontend/packages/dashboard/src/core.js +++ b/frontend/packages/dashboard/src/core.js @@ -16,12 +16,8 @@ import { createTabStrip } from '@hive/shared/tabs.js'; // ─── derived state (own copies; this bundle has its own runtime) ────────── let tombstonesState = []; -// ContainerView list from the last /api/state snapshot — used by renderContainerLoad -// to splice in the configured cpu_quota/memory_max limits alongside live cgroup data. -let containersState = []; function syncFromSnapshot(s) { tombstonesState = (s.tombstones || []).slice(); - containersState = (s.containers || []).slice(); } // ─── kept state (tombstones) ────────────────────────────────────────────── @@ -173,12 +169,7 @@ function cloadMeter(pct) { return m; } -// Stash the last load rows so re-renders triggered by SSE state updates can -// call renderContainerLoad(lastLoadRows) without waiting for the next poll. -let lastLoadRows = []; - function renderContainerLoad(rows) { - lastLoadRows = rows; const root = $('container-load-section'); if (!root) return; if (!Array.isArray(rows) || rows.length === 0) { @@ -187,25 +178,12 @@ function renderContainerLoad(rows) { p.textContent = 'no running agent containers'; root.append(p); return; } - - // Build a name→ContainerView lookup so we can splice in the configured - // cpu_quota / memory_max limits (set in meta/resource-limits.json and - // resolved by the server) next to the live cgroup readings. - const cvByName = new Map(containersState.map((c) => [c.name, c])); - const table = document.createElement('table'); table.className = 'hive-stats-table'; - // "cpu cap" / "mem cap" show the configured ceilings from ContainerView - // (effective drop-in values, take effect on next start/restart). - // "limit" remains the live cgroup memory ceiling from /api/container-resources. table.innerHTML = 'agentcpumemory' - + 'peaklimitdisk' - + 'cpu cap' - + 'mem cap' - + ''; + + 'peaklimitdisk'; const tb = document.createElement('tbody'); for (const r of rows) { - const cv = cvByName.get(r.name); const tr = document.createElement('tr'); const name = document.createElement('td'); name.textContent = r.name; tr.append(name); const cpu = document.createElement('td'); cpu.className = 'num'; @@ -232,113 +210,7 @@ function renderContainerLoad(rows) { disk.title = 'state dir + container writable rootfs (shared nix store excluded); sampled every few minutes'; disk.textContent = (r.disk_bytes != null) ? cloadFmtBytes(Number(r.disk_bytes)) : '—'; tr.append(disk); - - // Configured CPU / memory caps from ContainerView (resolved effective - // values: per-agent override when set, hive-wide default otherwise). - const cpuCap = document.createElement('td'); cpuCap.className = 'num cload-cap'; - cpuCap.title = 'configured ceiling — takes effect on next start'; - cpuCap.textContent = cv?.cpu_quota || '—'; - tr.append(cpuCap); - const memCap = document.createElement('td'); memCap.className = 'num cload-cap'; - memCap.title = 'configured ceiling — takes effect on next start'; - memCap.textContent = cv?.memory_max || '—'; - tr.append(memCap); - - // S3T button toggles the inline edit row for this agent. - const actTd = document.createElement('td'); - const setBtn = document.createElement('button'); - setBtn.type = 'button'; - setBtn.className = 'btn btn-sm cload-set-btn'; - setBtn.textContent = 'S3T'; - setBtn.title = 'set CPU / memory cap for ' + r.name; - tr.append(actTd); - actTd.append(setBtn); tb.append(tr); - - // Inline edit row (hidden by default, toggled by the S3T button). - const editRow = document.createElement('tr'); - editRow.className = 'cload-edit-row'; - editRow.hidden = true; - const editTd = document.createElement('td'); - editTd.colSpan = 9; - editTd.className = 'cload-edit-cell'; - - const editForm = document.createElement('form'); - editForm.className = 'cload-edit-form'; - editForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const cpuInput = editForm.querySelector('.cload-cpu-input'); - const memInput = editForm.querySelector('.cload-mem-input'); - const errSpan = editForm.querySelector('.cload-edit-err'); - const submitBtn = editForm.querySelector('[type="submit"]'); - errSpan.hidden = true; - submitBtn.disabled = true; - try { - const body = new URLSearchParams({ - cpu_quota: cpuInput.value.trim(), - memory_max: memInput.value.trim(), - }); - const resp = await fetch( - '/api/resource-limits/' + encodeURIComponent(r.name), - { method: 'POST', body }, - ); - if (!resp.ok) { - errSpan.textContent = await resp.text().catch(() => 'error ' + resp.status); - errSpan.hidden = false; - return; - } - // Success: collapse the edit row. The SSE rescan will push updated - // ContainerView data (cpu_quota/memory_max) to containersState, - // triggering a re-render of the cap columns via refreshContainerLoad. - editRow.hidden = true; - setBtn.textContent = 'S3T'; - } catch (err) { - errSpan.textContent = String(err); - errSpan.hidden = false; - } finally { - submitBtn.disabled = false; - } - }); - - const cpuLabel = document.createElement('label'); - cpuLabel.className = 'cload-edit-label'; - cpuLabel.textContent = 'cpu quota'; - const cpuInput = document.createElement('input'); - cpuInput.type = 'text'; cpuInput.className = 'cload-cpu-input'; - cpuInput.placeholder = cv ? cv.cpu_quota : 'e.g. 200%'; - cpuInput.title = 'systemd CPUQuota= value (e.g. "400%"). empty = use hive default'; - cpuLabel.append(cpuInput); - - const memLabel = document.createElement('label'); - memLabel.className = 'cload-edit-label'; - memLabel.textContent = 'mem max'; - const memInput = document.createElement('input'); - memInput.type = 'text'; memInput.className = 'cload-mem-input'; - memInput.placeholder = cv ? cv.memory_max : 'e.g. 8G'; - memInput.title = 'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default'; - memLabel.append(memInput); - - const submitBtn = document.createElement('button'); - submitBtn.type = 'submit'; submitBtn.className = 'btn btn-restart cload-save-btn'; - submitBtn.textContent = 'S4V3'; - - const hintSpan = document.createElement('span'); - hintSpan.className = 'meta cload-edit-hint'; - hintSpan.textContent = '↺ restart to apply to a running container'; - - const errSpan = document.createElement('span'); - errSpan.className = 'cload-edit-err'; errSpan.hidden = true; - - editForm.append(cpuLabel, memLabel, submitBtn, hintSpan, errSpan); - editTd.append(editForm); - editRow.append(editTd); - tb.append(editRow); - - setBtn.addEventListener('click', () => { - const open = !editRow.hidden; - editRow.hidden = open; - setBtn.textContent = open ? 'S3T' : '✕'; - }); } table.append(tb); root.replaceChildren(table); @@ -440,19 +312,6 @@ const SSE_HANDLERS = { tombstonesState = (ev.tombstones || []).slice(); renderTombstones({ tombstones: tombstonesState }); }, - // When a container's state changes (e.g. after resource-limits update - // triggers rescan_containers_and_emit), update containersState in place - // so the cap columns in the LOAD table reflect the new configured values. - container_state_changed(ev) { - if (!ev.container) return; - const idx = containersState.findIndex((c) => c.name === ev.container.name); - if (idx >= 0) { - containersState[idx] = ev.container; - } else { - containersState.push(ev.container); - } - if (lastLoadRows.length) renderContainerLoad(lastLoadRows); - }, // Refresh the stale-perms sub-section when perm data changes (a ghost // was cleared, or perms were saved for an agent whose name collides). capabilities_changed(_ev) { diff --git a/frontend/packages/dashboard/src/system-sections.css b/frontend/packages/dashboard/src/system-sections.css index 1f848ac5..07aa6338 100644 --- a/frontend/packages/dashboard/src/system-sections.css +++ b/frontend/packages/dashboard/src/system-sections.css @@ -213,6 +213,12 @@ .rqe-node-arrow { color: var(--muted); } .rqe-node-log { margin-left: 0.1em; text-decoration: none; } +.rqe-step { + flex-basis: 100%; + margin: 0.1em 0 0 1.8em; + color: var(--cyan); + font-size: 0.85em; +} .rqe-error { flex-basis: 100%; margin: 0.3em 0 0; @@ -349,61 +355,6 @@ background: var(--red); } -/* Configured-cap columns: muted header so they read apart from live metrics. - The S3T button sits in its own column; keep it compact. */ -.cload-cap-th { - color: var(--muted); -} -.cload-cap { - color: var(--muted); -} -.cload-set-btn { - padding: 0.1em 0.5em; - font-size: 0.8em; -} - -/* Inline edit row — spans all columns, contains a mini-form. */ -.cload-edit-row td { - border-top: none; - padding-top: 0; -} -.cload-edit-cell { - padding: 0.2em 0.4em 0.6em 1.2em; -} -.cload-edit-form { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5em; -} -.cload-edit-label { - display: flex; - align-items: center; - gap: 0.3em; - font-size: 0.85em; - color: var(--muted); -} -.cload-cpu-input, -.cload-mem-input { - width: 7em; - font-size: 0.9em; -} -.cload-save-btn { - padding: 0.15em 0.6em; - font-size: 0.85em; -} -.cload-edit-hint { - font-size: 0.8em; -} -.cload-edit-err { - color: var(--red); - font-size: 0.85em; -} - -/* Remove the now-dead `.rqe-step` rule (the `step` sub-step label was - retired from the wire in `feat(job_queue): retire the now-off-wire step - sub-step label` — the rule has had no JS referents since then). */ - /* ─── K3PT ST4T3 › stale permission entries sub-section ──────────────────── Agents with explicit capability / tool-group entries in the JSON but no live container (renamed or manually-deleted agents). Lazy-loaded on tab diff --git a/hive-c0re/src/dashboard/lifecycle_ops.rs b/hive-c0re/src/dashboard/lifecycle_ops.rs index e05dd608..70d4acbf 100644 --- a/hive-c0re/src/dashboard/lifecycle_ops.rs +++ b/hive-c0re/src/dashboard/lifecycle_ops.rs @@ -187,76 +187,6 @@ pub(super) async fn post_resume( (StatusCode::OK, "ok").into_response() } -/// Form fields for `post_resource_limits`. Both fields are optional strings; -/// an empty value clears the per-agent override for that field, falling back -/// to the hive-wide default. -#[derive(Deserialize, Default)] -pub(super) struct ResourceLimitsForm { - #[serde(default)] - cpu_quota: String, - #[serde(default)] - memory_max: String, -} - -/// `POST /api/resource-limits/{name}` — write per-agent CPU/memory limit -/// overrides for `name`. -/// -/// An empty `cpu_quota` or `memory_max` field clears that field's override, -/// falling back to the hive-wide default. Both empty together removes the -/// agent's entry entirely. The new drop-in is written immediately — the -/// limits take effect on the next container start or restart. Triggers an -/// immediate rescan so `ContainerView.cpu_quota`/`memory_max` update on -/// the dashboard via SSE without waiting for the next periodic sweep. -pub(super) async fn post_resource_limits( - State(state): State, - AxumPath(name): AxumPath, - Form(form): Form, -) -> Response { - let logical = strip_container_prefix(&name); - if let Some(reject) = guard_agent_name(&state, &logical).await { - return reject; - } - let ident = match Ident::parse(&logical) { - Ok(i) => i, - Err(e) => return (StatusCode::BAD_REQUEST, format!("bad agent name: {e}")).into_response(), - }; - let cpu_quota = if form.cpu_quota.is_empty() { - None - } else { - Some(form.cpu_quota.as_str()) - }; - let memory_max = if form.memory_max.is_empty() { - None - } else { - Some(form.memory_max.as_str()) - }; - if let Some(v) = cpu_quota - && let Err(e) = crate::resource_limits::validate_cpu_quota(v) - { - return (StatusCode::UNPROCESSABLE_ENTITY, e).into_response(); - } - if let Some(v) = memory_max - && let Err(e) = crate::resource_limits::validate_memory_max(v) - { - return (StatusCode::UNPROCESSABLE_ENTITY, e).into_response(); - } - let limits = crate::resource_limits::AgentLimits { - cpu_quota: cpu_quota.map(str::to_owned), - memory_max: memory_max.map(str::to_owned), - }; - if let Err(e) = crate::meta::commit_resource_limits(ident.as_str(), &limits).await { - return error_response(&format!("set limits {logical}: {e:#}")); - } - let agent_dir = crate::paths::agent_runtime_dir(ident.as_str()); - let hive = state.coord.hive_env(); - let paths = crate::coordinator::Coordinator::agent_paths(ident.as_str(), agent_dir); - if let Err(e) = crate::lifecycle::write_dropins(ident.as_str(), &hive, &paths).await { - return error_response(&format!("write_dropins {logical}: {e:#}")); - } - state.coord.rescan_containers_and_emit().await; - (StatusCode::OK, "ok").into_response() -} - pub(super) async fn post_update_all(State(state): State) -> Response { let containers = lifecycle::list().await.unwrap_or_default(); for container in containers { diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index 1470d7fd..cdb35f55 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -195,10 +195,6 @@ pub async fn serve( .route("/api/rebuild/{name}", post(lifecycle_ops::post_rebuild)) .route("/api/pause/{name}", post(lifecycle_ops::post_pause)) .route("/api/resume/{name}", post(lifecycle_ops::post_resume)) - .route( - "/api/resource-limits/{name}", - post(lifecycle_ops::post_resource_limits), - ) .route("/api/update-all", post(lifecycle_ops::post_update_all)) .route( "/api/infra-container/{name}/{action}",