Merge pull request 'frontend: install command uses the node id from the mint form (node-26#4)' (#107) from feat/mint-panel-nodeid into main
Build & Deploy / Build & push images (push) Successful in 5m51s
Build & Deploy / Deploy to VM (push) Successful in 1m42s
Build & Deploy / Report a failed deploy (push) Skipped

Reviewed-on: #107
This commit was merged in pull request #107.
This commit is contained in:
2026-09-06 20:15:10 -04:00
+19 -8
View File
@@ -41,19 +41,26 @@ function EnrollmentTokensPanel() {
const [justMinted, setJustMinted] = useState<string | null>(null); const [justMinted, setJustMinted] = useState<string | null>(null);
const [cmdCopied, setCmdCopied] = useState(false); const [cmdCopied, setCmdCopied] = useState(false);
const [tokenCopied, setTokenCopied] = useState(false); const [tokenCopied, setTokenCopied] = useState(false);
// The label the operator typed for the token that was just minted — used as
// the node id in the install command below. Captured on mint because `label`
// itself is cleared afterward.
const [mintedLabel, setMintedLabel] = useState<string | null>(null);
// The paste-ready one-shot install command for a fresh Pi. node id and the // The paste-ready one-shot install command for a fresh Pi. The node id comes
// MQTT broker host aren't known here — left as placeholders the operator // from the label just entered (spaces → dashes; install.sh requires
// edits. C2 URL comes from this deployment's own config; the broker is the // [A-Za-z0-9_-]); if that yields nothing it falls back to a node-XXX
// documented mqtt.<domain> sibling of the api host (install.sh header). // placeholder. The MQTT broker host is the documented mqtt.<domain> sibling
// of the api host (install.sh header) — a DNS assumption the operator checks.
const c2Url = (process.env.NEXT_PUBLIC_C2_URL ?? "https://api.example.net").replace(/\/$/, ""); const c2Url = (process.env.NEXT_PUBLIC_C2_URL ?? "https://api.example.net").replace(/\/$/, "");
const mqttBroker = (() => { const mqttBroker = (() => {
try { return `mqtt.${new URL(c2Url).hostname.replace(/^api\./, "")}`; } try { return `mqtt.${new URL(c2Url).hostname.replace(/^api\./, "")}`; }
catch { return "mqtt.example.net"; } catch { return "mqtt.example.net"; }
})(); })();
const nodeIdForCmd =
(mintedLabel ?? "").trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9_-]/g, "") || "node-XXX";
const installCmd = justMinted const installCmd = justMinted
? `curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \\ ? `curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \\
| sudo bash -s -- --token ${justMinted} --node-id node-XXX \\ | sudo bash -s -- --token ${justMinted} --node-id ${nodeIdForCmd} \\
--c2-url ${c2Url} --mqtt-broker ${mqttBroker}` --c2-url ${c2Url} --mqtt-broker ${mqttBroker}`
: ""; : "";
@@ -74,6 +81,7 @@ function EnrollmentTokensPanel() {
try { try {
const result = await c2api.mintEnrollmentToken(label.trim()); const result = await c2api.mintEnrollmentToken(label.trim());
setJustMinted(result.token); setJustMinted(result.token);
setMintedLabel(label.trim());
setLabel(""); setLabel("");
load(); load();
} catch (err) { } catch (err) {
@@ -119,7 +127,10 @@ function EnrollmentTokensPanel() {
</div> </div>
<p className="text-xs text-indigo-200 font-mono mt-3 mb-1"> <p className="text-xs text-indigo-200 font-mono mt-3 mb-1">
…or run this on a fresh Pi (edit <span className="text-indigo-100">node-XXX</span> and the broker host): …or run this on a fresh Pi{" "}
{nodeIdForCmd === "node-XXX"
? <>(edit <span className="text-indigo-100">node-XXX</span> and check the broker host)</>
: <>(check the broker host)</>}:
</p> </p>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<pre className="flex-1 text-xs text-indigo-100 font-mono whitespace-pre-wrap break-all bg-gray-900 rounded px-2 py-1.5">{installCmd}</pre> <pre className="flex-1 text-xs text-indigo-100 font-mono whitespace-pre-wrap break-all bg-gray-900 rounded px-2 py-1.5">{installCmd}</pre>
@@ -136,7 +147,7 @@ function EnrollmentTokensPanel() {
<button <button
type="button" type="button"
onClick={() => setJustMinted(null)} onClick={() => { setJustMinted(null); setMintedLabel(null); }}
className="text-xs text-indigo-300 hover:text-indigo-200 mt-3 transition-colors" className="text-xs text-indigo-300 hover:text-indigo-200 mt-3 transition-colors"
> >
Dismiss Dismiss
@@ -151,7 +162,7 @@ function EnrollmentTokensPanel() {
<input <input
value={label} value={label}
onChange={(e) => setLabel(e.target.value)} onChange={(e) => setLabel(e.target.value)}
placeholder="Label, e.g. 'node-003 field kit'" placeholder="Node ID, e.g. node-003"
className="flex-1 min-w-[12rem] bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500" className="flex-1 min-w-[12rem] bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
/> />
<Button type="submit" size="sm" disabled={minting || !label.trim()}> <Button type="submit" size="sm" disabled={minting || !label.trim()}>