// The Boardroom — ON-DEVICE DEBUG READOUT (?debug=1), shared by BOTH surfaces.
//
// EXTRACTED FROM app.jsx (GUEST-VIEWPORT-V1, 2026-07-29). It lived in app.jsx, which
// guest.html does not load — so a guest-side problem could not be diagnosed at all without
// signing in, and every iOS guest report had to be taken on description. Mike asked for it on
// the guest surface; the honest way to give it to him is one component both pages load, not a
// second copy that drifts from this one.
//
// It takes exactly ONE prop (`voice`); everything else it reports is read live off window/
// document, which is why it ports without a prop chain. `voiceChatAvailable` comes from
// voicechat.jsx, which both pages already load.
(function () {
// ── On-device evidence surface (incident 2026-07-17) ─────────────────────────────
// Headless gates passed while the real Pixel failed THREE times. This ugly readout — gated
// behind ?debug=1 (or #debug, or BR_CONFIG.debug) — shows the running truth ON the phone:
// which bundle/deploy is live, kokoroTTS as PARSED here, whether kokoro.js loaded, which
// speaker was chosen, the per-sentence TTS outcome (kokoro-ok / fallback+reason), the computed
// --rm-crawl-h + whether the pinned card exists, and the SW controller. Cheap, temporary.
function DebugReadout({ voice }) {
  const on = (typeof location !== "undefined" && /(?:[?&#])debug=1|#debug/.test((location.search || "") + (location.hash || ""))) ||
             !!((window.BR_CONFIG || {}).debug);
  const [, force] = React.useState(0);
  const [open, setOpen] = React.useState(false);   // collapsed by default — must NOT block the entry button
  React.useEffect(() => { if (!on) return undefined; const t = setInterval(() => force((x) => x + 1), 800); return () => clearInterval(t); }, [on]);
  // Scroll-target probe (round 3): capture-phase, so it sees EVERY scroll and names which
  // element actually scrolled — the whole question is "is the card the scroller, or the page?".
  React.useEffect(() => {
    if (!on || window.__BR_SCROLLPROBE) return undefined;
    window.__BR_SCROLLPROBE = true;
    window.addEventListener("scroll", (e) => {
      const t = e.target;
      window.__BR_SCROLLER = (t === document || t === window) ? "PAGE/document"
        : "." + String(t.className || t.tagName || "?").trim().split(/\s+/).slice(0, 2).join(".");
    }, { capture: true, passive: true });
  }, [on]);
  if (!on) return null;
  const cfg = window.BR_CONFIG || {};
  const cs = (typeof getComputedStyle !== "undefined" && typeof document !== "undefined") ? getComputedStyle(document.documentElement) : null;
  const crawlTop = cs ? (cs.getPropertyValue("--rm-crawl-top") || "").trim() : "";
  const crawlLeft = cs ? (cs.getPropertyValue("--rm-crawl-left") || "").trim() : "";
  const crawlW = cs ? (cs.getPropertyValue("--rm-crawl-w") || "").trim() : "";
  const card = (typeof document !== "undefined") ? document.querySelector(".rm-scroll.voice-crawl") : null;
  const cardCS = card ? getComputedStyle(card) : null;
  const cardRect = card ? card.getBoundingClientRect() : null;
  const mainEl = (typeof document !== "undefined") ? document.querySelector(".br-main") : null;
  const mainCS = mainEl ? getComputedStyle(mainEl) : null;
  const tts = (window.__BR_TTS || []).slice(-5);
  // Filtered to entries that actually carry numbers, so a malformed record can never make the
  // mean read as a level.
  const levels = (window.__BR_LEVELS || []).filter((x) => x && typeof x.rms === "number" && isFinite(x.rms));
  // ROUND 2 INSTRUMENTATION (2026-07-18, Mike: "instrument, don't just fix again") — live
  // anchorY snapshot (from autoscroll.js's single source of truth) + the board's-view
  // container's computed width/overflow, both read fresh every render tick.
  const anchorSet = window.__BR_ANCHOR_SET || null;
  const viewBody = (typeof document !== "undefined") ? document.querySelector(".rm-view-body") : null;
  const viewCS = viewBody ? getComputedStyle(viewBody) : null;
  const transitionLog = (window.__BR_TRANSITION_LOG || []).slice(-8);
  const scrollActionLog = (window.__BR_SCROLL_LOG || []).slice(-8);
  const ctrl = (navigator.serviceWorker && navigator.serviceWorker.controller)
    ? navigator.serviceWorker.controller.scriptURL.split("/").slice(-1)[0] : "none";
  // iOS audit instrumentation (2026-07-19) — read the diagnostic globals kokoro.js,
  // wakelock.js and voicechat.jsx populate (never written here; this is display-only).
  const srec = window.__BR_SPEECHREC || {};
  const aunlock = window.__BR_AUDIO_UNLOCK || {};
  const wlstate = window.__BR_WAKELOCK_STATE || {};
  const vv2 = (typeof window !== "undefined") ? window.visualViewport : null;
  const rows = [
    ["build", window.BR_BUILD || "MISSING — STALE BUNDLE"],
    ["kokoroTTS(parsed)", String(cfg.kokoroTTS)],
    ["voiceChat", String(cfg.voiceChat)],
    ["kokoro.js loaded", typeof window.createKokoroSpeaker],
    ["speaker chosen", window.__BR_SPEAKER || "(not entered voice)"],
    ["tts endpoint", window.__BR_TTS_ENDPOINT || "(n/a)"],
    ["autoscroll.js", typeof window.BR_AUTOSCROLL + " ver " + (((window.BR_AUTOSCROLL || {}).VERSION) || "PRE-ANCHOR (STALE)")],
    ["crawl-top/left/w", [crawlTop, crawlLeft, crawlW].map((x) => x || "UNSET").join(" ")],
    ["viewport", window.innerHeight + "ih / vv " + (window.visualViewport
      ? Math.round(window.visualViewport.height) + "+" + Math.round(window.visualViewport.offsetTop) : "none")],
    // IOS-LAYOUT-V1 (2026-07-29) — the two rows that SETTLE the diagnosis rather than assume it.
    // `layout` is the layout-viewport height; `visual` is what the user can see. On Android they
    // agree and gap=0. On iOS, gap>0 IS the defect: it is exactly how far below the screen a
    // `bottom:0` fixed element was being placed, and how much taller than the screen a 100dvh
    // shell was. If Mike's screenshot shows gap=0, my diagnosis is wrong and the cause is
    // elsewhere — which is worth knowing from one screenshot rather than another round.
    ["ios vv gap", (() => {
      const g = window.__BR_VIEWPORT;
      if (!g) return "no visualViewport (fallbacks in force)";
      return "layout=" + g.layoutH + " visual=" + g.h + " top=" + g.top
        + "  gap=" + g.bottom + (g.bottom > 0 ? "  <-- fixed/dvh overflow the screen by this" : "  (agree)");
    })()],
    // Does the DOCUMENT scroll? It must not: the shell owns scrolling via .br-main. A non-zero
    // scrollY, or a scrollHeight above the client height, is the top bar's route off the screen.
    ["doc scroll (hdr)", (() => {
      const de = document.documentElement;
      const bar = document.querySelector(".br-topbar");
      const r = bar ? bar.getBoundingClientRect() : null;
      return "y=" + Math.round(window.scrollY || 0)
        + " docH=" + (de ? de.scrollHeight + "/" + de.clientHeight : "?")
        + "  topbar.top=" + (r ? Math.round(r.top) + (Math.round(r.top) < 0 ? " CLIPPED ABOVE" : " flush") : "absent");
    })()],
    ["cards rm/crawl", document.querySelectorAll(".rm-scroll").length + "/" + document.querySelectorAll(".rm-scroll.voice-crawl").length],
    ["card computed", cardCS ? cardCS.position + " h=" + cardCS.height + " ovf=" + cardCS.overflowY : "n/a"],
    ["card client/scroll", card ? card.clientHeight + "/" + card.scrollHeight : "n/a"],
    ["card rect t/b", cardRect ? Math.round(cardRect.top) + "/" + Math.round(cardRect.bottom) : "n/a"],
    ["page lock (.br-main)", mainCS ? mainCS.overflowY : "n/a"],
    ["scrolling el", window.__BR_SCROLLER || "(none seen yet)"],
    ["spoken anchor", window.__BR_ANCHOR || "(none)"],
    ["anchorY (live)", anchorSet
      ? "y=" + (anchorSet.y == null ? "null" : Math.round(anchorSet.y)) + " following=" + anchorSet.following
        + " age=" + (Date.now() - anchorSet.t) + "ms\n                     set by: " + anchorSet.reason
      : "(never set)"],
    ["board's-view w/ovf", viewBody
      ? "client=" + viewBody.clientWidth + " scroll=" + viewBody.scrollWidth
        + " " + (viewBody.scrollWidth > viewBody.clientWidth ? "OVERFLOWING by " + (viewBody.scrollWidth - viewBody.clientWidth) + "px" : "ok")
        + "\n                     css: width=" + (viewCS ? viewCS.width : "?") + " max-width=" + (viewCS ? viewCS.maxWidth : "?")
        + " overflow-wrap=" + (viewCS ? viewCS.overflowWrap : "?") + " box-sizing=" + (viewCS ? viewCS.boxSizing : "?")
      : "(.rm-view-body not present)"],
    // CONTEXT-TRUNCATION (2026-07-29): a paste over the cap is truncated by ruling, but the
    // amount lost was only ever reconstructable from word counts. This reports what the page
    // actually RECEIVED against what it kept, so a device answers "was it our 30,000 cap or
    // something upstream of it?" directly rather than by inference.
    // THE PASTE CHAIN — clipboard -> field -> request, one row per hop. Whichever number
    // first falls short of the source is the clipper (2026-07-29).
    ["paste chain", (window.__BR_PASTE && window.__BR_PASTE.length)
      ? window.__BR_PASTE.map(function (p) {
          return Object.keys(p).map(function (k) { return k + "=" + p[k]; }).join(" ");
        }).join("\n                     ")
      : "(nothing pasted this session)"],
    ["last error", window.__BR_LASTERR || "(none)"],
    ["pinned card el", card ? "present" : "ABSENT — RoomScreen stale?"],
    ["voice.state", voice ? voice.state : "(not in voice)"],
    ["SW controller", ctrl],
    ["TTS outcomes", tts.length ? tts.map((x) => x.outcome + (x.reason ? ("(" + x.reason + ")") : "")).join("  ") : "(none yet — speak a turn)"],
    // IOS-AUDIO-LEVEL (2026-07-29). Peak/RMS dBFS of the audio THIS DEVICE received, measured
    // from the raw PCM in kokoro.js. It answers one question and rules out another: if these
    // read normal on the quiet phone, the bytes are fine and the loss is in the device's audio
    // ROUTE, not in what we serve. Reference for the eye: 100 server-side captures measured
    // median -20.7 LUFS with peaks -1.9 to -7.8 dBFS, which is level with YouTube speech.
    ["tts level (dBFS)", levels.length
      ? levels.slice(-6).map((x) => x.member.split("-")[0] + " pk" + x.peak + "/rms" + x.rms).join("  ")
        + "   mean rms " + (Math.round(levels.reduce((s, x) => s + x.rms, 0) / levels.length * 10) / 10)
      : "(none yet — speak a turn with ?debug=1)"],
    // iOS audit (2026-07-19): SpeechRecognition availability/config + display mode — the
    // question "does it even see the API, and is this a Home Screen install" at a glance.
    // [CORRECTED 2026-07-29] This read the RECORDED snapshot, which only exists once a
    // recognition object has been created — so before entering voice it printed
    // "native=false webkit=false" and looked like the API was missing on the device. It was
    // measuring "not measured yet". The interfaces are always knowable, so read them LIVE and
    // say plainly when the recorded half is still empty.
    ["speechrec iface", "native=" + !!window.SpeechRecognition + " webkit=" + !!window.webkitSpeechRecognition
      + " (live) · voiceChatAvailable=" + (typeof voiceChatAvailable === "function" ? voiceChatAvailable() : "?")
      + " displayMode=" + (srec.displayMode || "(not entered mic yet)")],
    // IOS-AUDIO-DUCKING (2026-07-29) — the row that settles it. `at playback` is the mic's real
    // state at the instant audio began, which is the only moment that matters: iOS ducks media
    // while the page holds a RECORD-category audio session. RELEASED means the half-duplex law
    // held and any remaining ducking is WebKit failing to drop the session (nothing in JS can
    // force that). LIVE would mean the law is being bypassed on some path — a different bug,
    // and ours.
    ["mic at playback", (() => {
      const m = window.__BR_MIC;
      if (!m) return "(none yet — speak a turn)";
      const p = m.atPlayback;
      return (p ? (p.live ? "LIVE <-- law bypassed, this is the bug" : "released (law held)")
                    + " at last playback" + (p.kind ? " [" + p.kind + "]" : "")
                : "(no playback yet)")
        + "   armed×" + (m.armed || 0) + " released×" + (m.released || 0)
        + (m.last ? "  last=" + m.last.event + (m.last.reason ? "(" + m.last.reason + ")" : "") : "");
    })()],
    ["speechrec config", "continuous=" + srec.continuous + " interim=" + srec.interimResults
      + " created=" + (srec.createdCount || 0) + "x" + (srec.lastCreatedAt ? " last=" + (Date.now() - srec.lastCreatedAt) + "ms ago" : "")],
    ["speechrec fired", "onresult=" + !!srec.onresultFired + " onend=" + !!srec.onendFired
      + (srec.lastError ? " lastErr=" + srec.lastError + " (" + (Date.now() - srec.lastErrorAt) + "ms ago)" : "")],
    ["audio unlock", "kokoro=" + (aunlock.kokoro ? (aunlock.kokoro.playResolved == null ? "pending" : aunlock.kokoro.playResolved)
        + (aunlock.kokoro.playError ? "(" + aunlock.kokoro.playError + ")" : "") : "n/a")
      + "  speechSynthesis=" + (aunlock.speechSynthesis ? (aunlock.speechSynthesis.started ? "started" : (aunlock.speechSynthesis.error || "pending")) : "n/a")],
    ["wake lock", "supported=" + !!wlstate.supported + " granted=" + !!wlstate.sentinelGranted
      + (wlstate.rejectReason ? " reject=" + wlstate.rejectReason : "") + "  video=" + !!wlstate.videoPlaying
      + (wlstate.videoPlayError ? " verr=" + wlstate.videoPlayError : "")
      + "  hidden×" + ((wlstate.hiddenTransitions || []).length)],
    ["viewport vv", vv2 ? ("h=" + Math.round(vv2.height) + " scale=" + vv2.scale + " ofs=" + Math.round(vv2.offsetTop)) : "none"],
  ];
  // Anchored bottom-LEFT and collapsed to a thin chip by default, so it never covers the header
  // "Chat with the board" entry button (top) — the exact control this readout exists to test.
  // Tap to expand upward (scrollable); tap the header to collapse. z-index above everything.
  // IOS-LAYOUT-V1 (2026-07-29). This was `bottom: 6` and was UNREACHABLE on iPhone: a fixed
  // element is laid out against the LAYOUT viewport, which while Safari's toolbars are showing
  // extends below the visible area — so the chip sat off-screen and could only be glimpsed by
  // dragging against the rubber-band, springing back before it could be tapped.
  // The visible bottom edge, in the coordinates a fixed element actually uses, is
  // `--vv-top + --vvh` (viewport.js). Anchor the panel's own bottom edge there with a
  // translate, which needs no knowledge of its height — so it pins correctly both collapsed
  // (a one-line chip) and expanded (a tall scrolling panel).
  // Fallbacks are the old behaviour exactly: with no visualViewport, 100dvh/100vh resolve as
  // before and --vv-top is 0.
  // The anchoring itself lives in ONE place — `.br-vv-pin` in viewport.js — so the dock and the
  // guest foot can be moved onto the same rule without a second copy of the maths to drift.
  const base = { left: 6, zIndex: 100000, background: "rgba(0,0,0,0.92)",
    color: "#00ff6a", font: "11px/1.35 ui-monospace, monospace", border: "1px solid #00ff6a", borderRadius: 5 };
  if (!open) {
    return (
      <div onClick={() => setOpen(true)} role="button" aria-label="Show debug readout"
        className="br-vv-pin"
        style={{ ...base, padding: "4px 8px", cursor: "pointer", maxWidth: "60vw", overflow: "hidden",
          textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
        {"⌗ debug ▸ " + (window.BR_BUILD || "?").slice(0, 8)}
      </div>
    );
  }
  return (
    // Expanded height is capped against the VISUAL viewport too — a 60vh panel on iOS is 60% of
    // a box taller than the screen, so the bottom of a long readout was unreachable even once
    // the chip could be tapped.
    <div className="br-vv-pin" style={{ ...base, right: 6, padding: 0,
                  display: "flex", flexDirection: "column" }}>
      <div onClick={() => setOpen(false)} role="button" aria-label="Collapse debug readout"
        style={{ cursor: "pointer", padding: "5px 8px", borderBottom: "1px solid #00ff6a", fontWeight: 700, flex: "none" }}>
        {"⌗ BR DEBUG ▾  (tap to collapse)"}
      </div>
      <div style={{ padding: "6px 8px", whiteSpace: "pre-wrap", overflow: "auto" }}>
        {rows.map((r) => (r[0] + ":").padEnd(20) + " " + r[1]).join("\n")}
        {"\n\n— voice-state transitions (scrollTop AT that moment; newest last) —\n"}
        {transitionLog.length
          ? transitionLog.map((e) => "  " + e.label.padEnd(32) + " top=" + e.scrollTop + " (max=" + (e.scrollHeight - e.clientHeight) + ")").join("\n")
          : "  (none yet — enter voice)"}
        {"\n\n— scroll ACTIONS (every toLive() ASK, honoured or suppressed; newest last) —\n"}
        {scrollActionLog.length
          ? scrollActionLog.map((e) => "  " + (e.suppressed ? "SUPPRESSED " : "ran:       ") + e.action.padEnd(46)
              + " " + e.from + "→" + e.to + " (anchorY=" + (e.anchorY == null ? "null" : Math.round(e.anchorY)) + ", max=" + e.maxTop + ")").join("\n")
          : "  (none yet)"}
      </div>
    </div>
  );
}
  if (typeof window !== "undefined") window.DebugReadout = DebugReadout;
})();
