import { useEffect } from "preact/hooks";
import { signal } from "@preact/signals";
import { apiPutJSON } from "../../lib/api.js";
import { HostSuggestionSheet } from "../HostSuggestionSheet.jsx";
const nodes = signal(null); const loadFailed = signal(false); const status = signal(null); const name = signal("");
const target = signal("");
const pickerOpen = signal(false);
function flash(msg, ok) {
status.value = { msg, ok };
}
function markLoadFailed(msg) {
nodes.value = null;
loadFailed.value = true;
flash(msg, false);
}
async function saveList(next) {
let r;
try {
r = await apiPutJSON("/api/settings/nodes", { nodes: next });
} catch (e) {
flash(`Save failed: ${e.message}`, false);
return false;
}
if (!r.ok) {
flash(`Save failed: PUT /api/settings/nodes -> ${r.status}`, false);
return false;
}
nodes.value = next;
flash("Saved ✓", true);
return true;
}
export function NodesCard() {
const load = async () => {
loadFailed.value = false;
let res;
try {
res = await fetch("/api/settings/nodes", {
headers: { Accept: "application/json" },
redirect: "manual",
});
} catch (e) {
markLoadFailed(`Failed to load nodes: ${e.message}`);
return;
}
if (res.status === 404 || res.type === "opaqueredirect") {
nodes.value = [];
loadFailed.value = false;
flash("This server has no node support — update mobux.", false);
return;
}
if (!res.ok) {
res.text().catch(() => {});
markLoadFailed(
`Failed to load nodes: GET /api/settings/nodes -> ${res.status}`,
);
return;
}
let d;
try {
d = await res.json();
} catch (e) {
markLoadFailed(`Failed to load nodes: ${e.message}`);
return;
}
nodes.value = d.nodes || [];
loadFailed.value = false;
status.value = null;
};
useEffect(() => {
nodes.value = null;
loadFailed.value = false;
pickerOpen.value = false;
load();
}, []);
const add = async (e) => {
e.preventDefault();
if (nodes.value == null) {
flash("Nodes haven't loaded yet — retry before adding.", false);
return;
}
const n = name.value.trim();
const t = target.value.trim();
if (!n || !t) {
flash("Both name and SSH target are required.", false);
return;
}
if (nodes.value.some((x) => x.name === n)) {
flash(`A node named '${n}' already exists.`, false);
return;
}
const ok = await saveList([...nodes.value, { name: n, target: t }]);
if (ok) {
name.value = "";
target.value = "";
}
};
const remove = (n) => {
if (nodes.value == null) return;
saveList(nodes.value.filter((x) => x.name !== n));
};
const list = nodes.value;
const editable = list != null;
return (
<section class="settings-group" id="nodes-settings">
<h2>Nodes</h2>
<p class="settings-lede">
Remote hosts this hub can open tmux sessions on over SSH. SSH keys are
your job: the hub's key must already be authorized on the node — there
is no key management here.
</p>
{list == null && !loadFailed.value && <p class="hint">Loading…</p>}
{list == null && loadFailed.value && (
<p class="hint">
Could not load the node list — editing is disabled until it does.{" "}
<button type="button" id="nodesRetryBtn" onClick={load}>
Retry
</button>
</p>
)}
{list &&
list.map((n) => (
<div class="settings-row node-row" data-name={n.name} key={n.name}>
<span class="node-name">{n.name}</span>
<span class="node-target">{n.target}</span>
<button
type="button"
class="node-remove"
aria-label={`Remove ${n.name}`}
onClick={() => remove(n.name)}
>
Remove
</button>
</div>
))}
{list && list.length === 0 && (
<p class="hint">No nodes configured. Sessions run on this host.</p>
)}
<form class="node-add" onSubmit={add}>
<input
id="nodeName"
class="settings-input"
placeholder="name (e.g. devbox)"
autocomplete="off"
disabled={!editable}
value={name.value}
onInput={(e) => (name.value = e.target.value)}
/>
<input
id="nodeTarget"
class="settings-input"
placeholder="user@host"
autocomplete="off"
readOnly
disabled={!editable}
value={target.value}
onClick={() => editable && (pickerOpen.value = true)}
/>
<button type="submit" id="nodeAddBtn" disabled={!editable}>
Add
</button>
</form>
<HostSuggestionSheet
open={pickerOpen.value}
value={target.value}
onChange={(v) => (target.value = v)}
onClose={() => (pickerOpen.value = false)}
/>
{status.value && (
<div
id="nodesStatus"
class="settings-status"
style={{ color: status.value.ok ? "#7ec87e" : "#c87e7e" }}
>
{status.value.msg}
</div>
)}
</section>
);
}