const { useState: useStateD, useEffect: useEffectD, useRef: useRefD, useMemo: useMemoD } = React; const LEVEL_COLORS = { DEBUG: { tag:'var(--ink-4)' }, INFO: { tag:'var(--info)' }, WARN: { tag:'var(--warn)' }, ERROR: { tag:'var(--bad)' }, }; function fmtLogTime(ts) { if (typeof ts !== "number") return String(ts || ""); const d = new Date(ts * 1000); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; } function dayKey(ts) { const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function dayLabel(dk) { const [y, m, d] = dk.split("-").map(Number); const target = new Date(y, m - 1, d); const today = new Date(); today.setHours(0, 0, 0, 0); const diff = Math.round((today - target) / 86400000); if (diff === 0) return "today"; if (diff === 1) return "yesterday"; return target.toLocaleDateString(undefined, { day: "numeric", month: "short" }); } function InstanceDetail({ instance, codes, onClose, onLaunch, onStop, onReset }) { if (!instance) return null; const isRunning = instance.status === "running" || instance.status === "booting"; const logs = useLiveLogs(instance.id, { limit: 500 }); const tailRef = useRefD(null); const [autoscroll, setAutoscroll] = useStateD(true); const [filter, setFilter] = useStateD({ DEBUG: true, INFO: true, WARN: true, ERROR: true }); const [levelOpen, setLevelOpen] = useStateD(false); const [tab, setTab] = useStateD("current"); const [selectedDay, setSelectedDay] = useStateD(null); useEffectD(() => { if (!autoscroll || !tailRef.current) return; tailRef.current.scrollTop = tailRef.current.scrollHeight; }, [logs, autoscroll]); const onScroll = () => { if (!tailRef.current) return; const el = tailRef.current; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; setAutoscroll(atBottom); }; const runBoundary = useMemoD(() => { for (let i = logs.length - 1; i >= 0; i--) { const m = logs[i].msg || ""; if (m.includes("command: launch") || m.includes("task=work_flow")) { return logs[i].ts; } } return null; }, [logs]); const days = useMemoD(() => { const s = new Set(); logs.forEach(l => s.add(dayKey(l.ts))); return Array.from(s).sort().reverse(); }, [logs]); useEffectD(() => { if (tab === "day" && !selectedDay && days.length > 0) { setSelectedDay(days[0]); } }, [tab, days, selectedDay]); const visibleLogs = useMemoD(() => logs.filter(l => { if (!filter[l.level]) return false; if (tab === "current") { return runBoundary !== null && l.ts >= runBoundary; } if (tab === "day") { return selectedDay && dayKey(l.ts) === selectedDay; } return true; }), [logs, filter, tab, selectedDay, runBoundary]); const activeLevels = Object.entries(filter).filter(([_, v]) => v).map(([k]) => k); const levelLabel = activeLevels.length === 4 ? "all" : activeLevels.length === 0 ? "none" : activeLevels.join("·"); const [shotOpen, setShotOpen] = useStateD(false); return ( ); } function LogLine({ line }) { const c = LEVEL_COLORS[line.level] || LEVEL_COLORS.INFO; return (
{fmtLogTime(line.ts)} {line.level} {line.msg}
); } // Реальный скриншот: шлём команду боту через /api/control, ждём WS-событие function ScreenshotModal({ instance, onClose }) { const [state, setState] = useStateD("requesting"); const [imgUrl, setImgUrl] = useStateD(null); const padRef = useRefD(instance.id); const fetchShot = useRefD(async () => { try { const r = await fetch(`/api/screenshot/${encodeURIComponent(padRef.current)}?t=${Date.now()}`); if (!r.ok) throw new Error("no shot"); const blob = await r.blob(); setImgUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return URL.createObjectURL(blob); }); setState("ready"); } catch { setState("error"); } }); const request = useRefD(async () => { setState("requesting"); const ok = await Live.control([padRef.current], "screenshot"); if (!ok) { setState("error"); return; } }); useEffectD(() => { request.current(); const off = Live.subscribe((m) => { if (m.type === "screenshot_ready" && m.pad_code === padRef.current) { fetchShot.current(); } }); const t = setTimeout(() => { fetchShot.current(); }, 12000); return () => { off(); clearTimeout(t); }; }, [instance.id]); return ( <>
Screenshot {instance.name}
{imgUrl && Save}
{state === "ready" && imgUrl ? ( screenshot ) : (
{state === "error" ? ( <> не удалось получить скриншот ) : ( capturing… )}
)}
); } Object.assign(window, { InstanceDetail });