// 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 ( ); } 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"}
    {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 ? : }
    )} {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.

    ) : (
    { setHandle(e.target.value); setNote(null); }} />
    )} {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 }; })();