Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/shared/src/forms.js
atlas 4aa11b3a94 build agents only from internal/meta; delete the hive's local meta flake
The hive no longer renders, locks or commits a meta flake. A rebuild's
MetaSync node picks the internal/meta commit and records it with the
hyperhive and config pins it holds; Prebuild, Swap and Create build from it,
and a head that does not pin the agent's current config fails the build
naming the agent. hive-priv's meta_rev is required.

Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot
sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its
panel, MetaWindow, the hive's hyperhive_rev setting.

Kept: agent_set::refresh holds what sync_agents also did (topology.json, the
gateway's agent-socket map and nginx include), called from spawn, destroy,
rebuild and the migration.

The boot reconcile and the stale-start upgrade compare the agent's marker
with the hyperhive pin of internal/meta; the dashboard's deployed chip reads
the config marker a successful build writes.
2026-10-25 15:32:16 +01:00

115 lines
4.5 KiB
JavaScript

// Shared async-form primitives. Used by both the dashboard and the
// per-agent UI for any button/form that triggers a network action.
// Two pieces: `asyncBtn` (below) is the low-level per-button primitive;
// `bindAsyncForms` (further down) is the page-level `data-async` form
// submit interceptor built on top of it + the themed dialogs in
// `modal.js`.
//
// `asyncBtn(btn, fn)` — the single reusable component:
// 1. Guards double-click: returns immediately if `btn` is already
// disabled (prevents a second identical request from firing).
// 2. Saves `btn.innerHTML` and replaces it with a spinner during the
// async operation.
// 3. Re-enables the button and restores the original content when `fn`
// resolves or rejects (via `finally`), so callers don't need
// save/restore boilerplate.
//
// Usage:
// btn.addEventListener('click', () => asyncBtn(btn, async () => {
// const resp = await fetch('/api/...');
// if (!resp.ok) throw new Error(await resp.text());
// // handle success
// }));
//
// Error handling: `asyncBtn` restores the button on any thrown error /
// rejected promise but does NOT surface the error — callers must catch
// and display it themselves (via `themedToast`, `alert`, a status span,
// etc.) inside `fn` without re-throwing. `fn` must not let errors escape
// unhandled: `asyncBtn` returns the `fn().finally(...)` promise so
// callers can optionally chain `.catch` or `await`, but does not add its
// own catch — an unhandled rejection from `fn` will propagate normally.
export function asyncBtn(btn, fn) {
if (btn.disabled) return; // double-click guard
const orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner">◐</span>';
return fn().finally(() => {
btn.disabled = false;
btn.innerHTML = orig;
});
}
// Page-level submit interceptor for `data-async` forms — the pattern every
// dashboard/agent action button uses (spawn, cancel, purge,
// rebuild, …). Without this a `data-async` form POSTs natively and the
// browser navigates to the bare `ok` response page. Each page that renders
// such forms must call this once at boot. `onSuccess` runs after a
// successful submit unless the form opts out with `data-no-refresh` (forms
// whose mutation arrives faster via an SSE event).
//
// Confirmation (`data-confirm`) is surfaced via the themed dialog in
// `modal.js` rather than native `confirm()`, and errors via `themedToast`
// rather than `alert()`, so every page gets the same in-theme experience.
import { themedConfirm, themedToast } from "./modal.js";
export function bindAsyncForms(onSuccess) {
document.addEventListener("submit", async (e) => {
const f = e.target;
if (!(f instanceof HTMLFormElement) || !f.hasAttribute("data-async"))
return;
e.preventDefault();
if (
f.dataset.confirm &&
!(await themedConfirm({ message: f.dataset.confirm }))
)
return;
const btn = f.querySelector(
'button[type="submit"], button:not([type]), .btn-inline',
);
// Inner action: POST, clear inputs, call onSuccess.
// Errors are surfaced via themedToast; the caller does not re-throw
// so asyncBtn's finally always runs (restoring the button).
const doSubmit = async () => {
let resp;
try {
resp = await fetch(f.action, {
method: f.method || "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(new FormData(f)),
redirect: "manual",
});
} catch (err) {
themedToast("action failed: " + err, { type: "error" });
return;
}
const ok =
resp.ok ||
resp.type === "opaqueredirect" ||
(resp.status >= 200 && resp.status < 400);
if (!ok) {
const text = await resp.text().catch(() => "");
themedToast(
"action failed: " + resp.status + (text ? "\n\n" + text : ""),
{ type: "error" },
);
return;
}
// Clear text inputs whose value was just submitted.
f.querySelectorAll(
'input[type="text"], input:not([type]), textarea',
).forEach((i) => {
i.value = "";
});
if (
!f.hasAttribute("data-no-refresh") &&
typeof onSuccess === "function"
) {
onSuccess();
}
};
// asyncBtn guards double-submit and shows a spinner while in-flight.
// When there is no submit button (unusual), fall through without a guard.
if (btn) asyncBtn(btn, doSubmit);
else await doSubmit();
});
}