// debug-hud.jsx — Sprint β passive observability overlay. // // Активация (опт-ин): // ?debug=1 в URL ← предпочтительно, чтобы случайно не включить // localStorage.setItem("debugHud", "1") в DevTools Console // // Принципы (из обсуждения β): // - Passive: НЕ trigger renders в обычных компонентах, // НЕ subscribes на каждое state change, // НЕ становится сам причиной lag. // - Reads only: poll-tick раз в 1 сек, читает из: // window.queryClient._cache (runtime.js) // window.__chatScrollStats (ChatScroll глобал) // fetch-wrapper counter (внутри HUD). // - Никаких subscriptions/observers — только interval read. // // Размер: ~150 строк. Свой scope, не интегрируется с runtime.js. (function () { "use strict"; function isEnabled() { if (typeof window === "undefined") return false; try { const params = new URLSearchParams(window.location.search); if (params.get("debug") === "1") return true; if (window.localStorage && window.localStorage.getItem("debugHud") === "1") return true; } catch {} return false; } // Fetch counter — wrapper устанавливается один раз при mount Toaster'a. // Если кто-то ещё обернул fetch — это ОК, мы цепляемся сверху. let fetchCounter = { total: 0, cancelled: 0, last60s: [] }; function _installFetchWatcher() { if (window.__hudFetchInstalled) return; window.__hudFetchInstalled = true; const orig = window.fetch; window.fetch = async function (input, init) { const startTs = Date.now(); fetchCounter.total++; fetchCounter.last60s.push(startTs); // Прибиваем хвост старше 60 сек const cutoff = startTs - 60_000; while (fetchCounter.last60s.length && fetchCounter.last60s[0] < cutoff) { fetchCounter.last60s.shift(); } if (init && init.signal) { init.signal.addEventListener("abort", () => { fetchCounter.cancelled++; }); } try { return await orig.apply(this, arguments); } catch (e) { if (e && e.name === "AbortError") fetchCounter.cancelled++; throw e; } }; } function _row(label, value, danger) { return React.createElement( "div", { className: "flex items-center justify-between gap-3" }, React.createElement("span", { className: "text-zinc-500" }, label), React.createElement( "span", { className: danger ? "text-rose-300" : "text-zinc-200 tabular-nums" }, value ) ); } function _section(title, children) { return React.createElement( "div", { className: "border-t border-zinc-800/60 pt-1.5 mt-1.5 first:border-0 first:pt-0 first:mt-0" }, React.createElement( "div", { className: "text-[10px] uppercase tracking-wider text-zinc-600 mb-1" }, title ), React.createElement("div", { className: "space-y-0.5" }, children) ); } function DebugHud() { const [, force] = React.useState(0); const [collapsed, setCollapsed] = React.useState(false); React.useEffect(() => { _installFetchWatcher(); const id = setInterval(() => force((n) => n + 1), 1000); return () => clearInterval(id); }, []); if (!isEnabled()) return null; // === Сбор данных (read-only) === const qc = window.queryClient || { _cache: new Map(), _listeners: new Map() }; const cacheEntries = qc._cache instanceof Map ? qc._cache.size : 0; const subscribers = qc._listeners instanceof Map ? Array.from(qc._listeners.values()).reduce((s, v) => s + (v ? v.size : 0), 0) : 0; const cs = window.__chatScrollStats || {}; const pollingHidden = typeof document !== "undefined" ? document.hidden : false; if (collapsed) { return React.createElement( "button", { onClick: () => setCollapsed(false), className: "fixed bottom-3 right-3 z-[9998] px-2 py-1 rounded bg-zinc-900/90 border border-zinc-700 text-[10px] text-zinc-400 font-mono backdrop-blur", title: "Открыть Debug HUD", }, "🔧 HUD" ); } return React.createElement( "div", { className: "fixed bottom-3 right-3 z-[9998] w-[260px] bg-zinc-950/95 border border-zinc-800 rounded-lg p-2.5 text-[11px] font-mono backdrop-blur shadow-2xl pointer-events-auto", }, React.createElement( "div", { className: "flex items-center justify-between mb-2 pb-1.5 border-b border-zinc-800/60" }, React.createElement( "span", { className: "text-zinc-400 uppercase tracking-wider text-[10px]" }, "Debug HUD" ), React.createElement( "button", { onClick: () => setCollapsed(true), className: "text-zinc-500 hover:text-zinc-300", title: "Свернуть", }, "−" ) ), _section("runtime", [ _row("cache entries", cacheEntries), _row("subscribers", subscribers), _row("fetches / 60s", fetchCounter.last60s.length), _row("fetches total", fetchCounter.total), _row("cancelled", fetchCounter.cancelled, fetchCounter.cancelled > 100), _row("polling paused", pollingHidden ? "✓ (hidden)" : "—"), ]), _section("chat scroll", [ _row( "chat id", cs.currentChatId ? String(cs.currentChatId).slice(0, 16) + "…" : "—" ), _row("messages", cs.messagesCount || 0), _row("atBottom", cs.atBottom ? "✓" : "—"), _row("hasMore", cs.hasMore ? "✓" : "—"), _row("loadingMore", cs.loadingMore ? "✓" : "—"), _row( "oldestTs", cs.oldestTs ? String(cs.oldestTs).slice(0, 16) : "—" ), ]), _section("β counters", [ _row("merges", cs.merges || 0), _row("prepends", cs.prepends || 0), _row("scroll restores", cs.scrollRestores || 0), _row("media re-anchors", cs.mediaLoadRestores || 0), _row("stale ignored", cs.staleIgnored || 0, (cs.staleIgnored || 0) > 10), ]) ); } window.DebugHud = DebugHud; })();