// You > Display > My boards (#/settings/display/my-boards) and Admin > Group admin > Board owners.
// Contract: contracts/ESP32_BOARD_OWNER_GROUP.md (blackbook-api #430). Rules + copy: utils/board-owner.js.
// Pages only, no modals. The group picker lists ONLY the groups[] the server returned for that board; the board
// changes on screen only after the server says 200 (no optimistic switch). Errors show inline on the board's row.
(function () {
const { useState, useEffect, useCallback } = React;
const B = () => window.BBBoardOwner;
function SignInButton({ navigate, next }) {
if (!navigate) return null;
return (
navigate(window.BBRouter ? window.BBRouter.build("signin", null, { next }) : "/signin")}>Sign in
);
}
function BoardRow({ board, apiBase, token, navigate, onSaved }) {
const BO = B();
const st = BO.pickerState(board);
const current = (st.choices.find((c) => c.current) || {}).value || "";
const [pick, setPick] = useState(current);
const [busy, setBusy] = useState(false);
const [note, setNote] = useState(null); // { kind: "ok" | "error", code, text, signIn }
useEffect(() => { setPick(current); }, [current]);
const id = board.id;
const save = async () => {
if (!pick || busy) return;
setBusy(true); setNote(null);
const r = await BO.request(apiBase, token, "PUT", BO.PATHS.group(id), { group_code: pick });
setBusy(false);
if (r.ok) {
const next = BO.applyGroupResult(board, r.j);
onSaved(next);
setNote({ kind: "ok", code: "200", text: BO.savedText(r.j, board) });
} else {
setNote({ kind: "error", ...BO.groupError(r.status, r.j) });
setPick(current);
}
};
const selId = `myboards-group-${id}`;
return (
{board.name || `Board ${id}`}
{BO.typeLabel(board)}
Showing
{BO.groupLabel(board.group_name, board.group_code)}
Owner
{board.owner_handle || "No owner yet"}
Group this board shows
{ setPick(e.target.value); setNote(null); }}>
{st.choices.length === 0 && {BO.groupLabel(board.group_name, board.group_code)} }
{st.choices.map((c) => {c.label} )}
{busy ? "Saving…" : "Save"}
{st.reason &&
{st.reason}
}
{note && (
{note.text}
{note.signIn && }
)}
);
}
function MyBoardsPage({ apiBase, token, navigate }) {
const BO = B();
const [st, setSt] = useState({ state: "loading", boards: [], err: null });
const load = useCallback(async () => {
if (!token) { setSt({ state: "error", boards: [], err: BO.listError(401) }); return; }
setSt((s) => ({ ...s, state: s.boards.length ? "ready" : "loading" }));
const r = await BO.request(apiBase, token, "GET", BO.PATHS.myBoards);
if (r.ok && r.j && Array.isArray(r.j.boards)) setSt({ state: "ready", boards: r.j.boards, err: null });
else setSt({ state: "error", boards: [], err: BO.listError(r.status) });
}, [apiBase, token]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { load(); }, [load]);
const onSaved = (next) => setSt((s) => ({ ...s, boards: s.boards.map((b) => (b.id === next.id ? next : b)) }));
return (
Boards on your account. Pick which of your groups an ESP32 board shows; its board and display settings follow your handle in that group.
{st.state === "loading" &&
Loading…
}
{st.state === "error" && st.err && (
{st.err.text}
{st.err.signIn ?
: Try again }
)}
{st.state === "ready" && st.boards.length === 0 && (
No boards on your account yet.
)}
{st.state === "ready" && st.boards.length > 0 && (
)}
);
}
// Admin > Group admin: set an ESP32 board's owner by handle. Rendered only for admins (isAdmin) and only boards the
// server lists for this admin (GET /admin/fleet/bench is scoped to the admin's group). Pi rows: field off (the Pi's
// owner follows its handle claim) and a 409 from the server is shown inline the same way.
function OwnerRow({ row, apiBase, token, onSaved }) {
const BO = B();
const [handle, setHandle] = useState("");
const [busy, setBusy] = useState(false);
const [note, setNote] = useState(null);
const isPi = row.type === "pi";
const id = row.id;
const save = async (e) => {
if (e) e.preventDefault();
if (busy || isPi) return;
const h = handle.trim();
if (!h) { setNote({ kind: "error", ...BO.ownerError(400, { detail: "handle required" }) }); return; }
setBusy(true); setNote(null);
const r = await BO.request(apiBase, token, "PUT", BO.PATHS.owner(id), { handle: h });
setBusy(false);
if (r.ok) {
onSaved(id, r.j.owner_handle || h);
setHandle("");
setNote({ kind: "ok", code: "200", text: r.j.changed === false ? `${r.j.owner_handle || h} already owns this board.` : `Saved. ${r.j.owner_handle || h} owns this board now.` });
} else {
setNote({ kind: "error", ...BO.ownerError(r.status, r.j) });
}
};
const inputId = `board-owner-input-${id}`;
return (
{row.name}
{row.type === "esp32" ? "ESP32" : row.type === "pi" ? "Pi" : "Board"}
Owner: {row.owner_handle || "No owner yet"}
{isPi ? (
A Pi's owner follows the handle it's signed in with, so it can't be changed here.
) : (
)}
{note && (
{note.text}
)}
);
}
function BoardOwnersCard({ apiBase, token, isAdmin }) {
const BO = B();
const [st, setSt] = useState({ state: "loading", rows: [] });
const load = useCallback(async () => {
if (!isAdmin || !token) return;
const [bench, esp] = await Promise.all([
BO.request(apiBase, token, "GET", BO.PATHS.bench),
BO.request(apiBase, token, "GET", BO.PATHS.esp32),
]);
if (!bench.ok && !esp.ok) { setSt({ state: "hidden", rows: [], status: bench.status }); return; }
const rows = BO.adminBoards(bench.ok ? bench.j.devices : [], esp.ok ? esp.j.boards : null);
setSt({ state: "ready", rows });
}, [apiBase, token, isAdmin]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { load(); }, [load]);
if (!isAdmin) return null;
const onSaved = (id, h) => setSt((s) => ({ ...s, rows: s.rows.map((r) => (r.id === id ? { ...r, owner_handle: h } : r)) }));
return (
Board owners
An ESP32 board shows its owner's group and display settings. Set the owner to an active member of this group by handle.
{st.state === "loading" && Loading…
}
{st.state === "hidden" && Couldn't load this group's boards.
}
{st.state === "ready" && st.rows.length === 0 && No boards in this group yet.
}
{st.state === "ready" && st.rows.length > 0 && (
)}
);
}
window.BBMyBoards = { MyBoardsPage, BoardOwnersCard };
})();