"""Пересъёмка скриншотов панели в docs/screenshots (нужен playwright + chromium). python -m omni serve --port 8077 # в одном окне python tools/shots_ui.py # в другом Каждый маршрут снимается только после того, как пропали скелетоны, иначе в кадр попадает «Загрузка находок...». Ресайз до 1600px по ширине — картинки для README, а не для печати. """ from __future__ import annotations import argparse import asyncio import pathlib from playwright.async_api import async_playwright ROUTES = ["feed", "abuse", "sources", "tg", "watch", "digest", "overview", "runs", "keys"] SKELETONS = ( "() => document.querySelectorAll(" "'[class*=skeleton],[class*=placeholder],[class*=spinner]').length" ) def resize(path: pathlib.Path, width: int = 1600) -> tuple[int, int]: from PIL import Image im = Image.open(path) if im.width > width: im = im.resize((width, round(im.height * width / im.width)), Image.LANCZOS) im.save(path, "PNG", optimize=True) return im.size async def shoot(base: str, out: pathlib.Path, only: list[str], scale: int) -> None: out.mkdir(parents=True, exist_ok=True) async with async_playwright() as p: browser = await p.chromium.launch( headless=True, args=["--no-sandbox", "--disable-gpu", "--hide-scrollbars", "--force-color-profile=srgb"], ) ctx = await browser.new_context( viewport={"width": 1600, "height": 1000}, device_scale_factor=scale, locale="ru-RU", ) page = await ctx.new_page() for route in only: await page.goto(f"{base}/#/{route}", wait_until="networkidle", timeout=90000) clean = False for _ in range(30): await page.wait_for_timeout(1000) if await page.evaluate(SKELETONS) == 0: await page.wait_for_timeout(1500) if await page.evaluate(SKELETONS) == 0: clean = True break target = out / f"{route}.png" await page.screenshot(path=str(target)) size = resize(target) print(f"{route:9s} clean={clean} -> {size} {target.stat().st_size} B") await browser.close() def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--base", default="http://127.0.0.1:8077") ap.add_argument("--out", default="docs/screenshots") ap.add_argument("--only", default="", help="через запятую: feed,abuse") ap.add_argument("--scale", type=int, default=2, help="device scale factor") a = ap.parse_args() only = [r.strip() for r in a.only.split(",") if r.strip()] or ROUTES asyncio.run(shoot(a.base, pathlib.Path(a.out), only, a.scale)) if __name__ == "__main__": main()