docs: скриншоты панели (9 разделов) + tools/shots_ui.py для пересъёмки; README — раздел «Скриншоты»
54
README.md
@@ -27,7 +27,7 @@ python -m omni.bot
|
|||||||
- **источников: 869** в 11 категориях (662 базовых + 207 из реестра абузов + 19 Telegram-каналов, дубли вырезаны), **записей в базе: 9889** (421 с ценой, 120 прогонов), из них 3135 из реестра абузов и 2966 из Telegram (превью + своя сессия)
|
- **источников: 869** в 11 категориях (662 базовых + 207 из реестра абузов + 19 Telegram-каналов, дубли вырезаны), **записей в базе: 9889** (421 с ценой, 120 прогонов), из них 3135 из реестра абузов и 2966 из Telegram (превью + своя сессия)
|
||||||
- **типы записей: 14** — каждая запись размечена по смыслу: новость 3693 · софт 516 · уязвимость 402 · релиз 361 · модель 331 · вопрос 265 · цена 205 · гайд 194 · датасет 126 · слив 90 · триал 41 · промо 34 · вакансия 21 · перепродажа 10
|
- **типы записей: 14** — каждая запись размечена по смыслу: новость 3693 · софт 516 · уязвимость 402 · релиз 361 · модель 331 · вопрос 265 · цена 205 · гайд 194 · датасет 126 · слив 90 · триал 41 · промо 34 · вакансия 21 · перепродажа 10
|
||||||
- **X/Twitter: 30 аккаунтов** читаются без API-ключей через `api.fxtwitter.com` (313 твитов в базе)
|
- **X/Twitter: 30 аккаунтов** читаются без API-ключей через `api.fxtwitter.com` (313 твитов в базе)
|
||||||
- вкладки панели: `Лента` · `Источники` · `Прогоны` · `Абузы`; полоса чипов «тип», фильтр «только с ценой», сортировка `score/fresh/price`, экспорт CSV
|
- разделы панели: `Лента` · `Находки` · `Источники` · `Telegram` · `Отслеживание` · `Дайджест` + `Система` (`Обзор` · `История сборов` · `API-ключи`); полоса чипов «тип», фильтр «только с ценой», сортировка `score/fresh/price`, экспорт CSV/JSON, тёмная и светлая темы
|
||||||
- health-аудит: живой/мёртвый/непроверенный по каждому источнику, причина в примечании
|
- health-аудит: живой/мёртвый/непроверенный по каждому источнику, причина в примечании
|
||||||
- Telegram-бот на той же базе: `/new`, `/abuse`, `/find`, `/src`, `/run`, `/add`, `/auto`, `/sum`, `/tr`, `/stats`
|
- Telegram-бот на той же базе: `/new`, `/abuse`, `/find`, `/src`, `/run`, `/add`, `/auto`, `/sum`, `/tr`, `/stats`
|
||||||
- тесты: `tests/test_regress.py` — 16 проверок на те же баги, что ловило ревью; CI: `.github/workflows/ci.yml` (py3.10/3.11/3.12)
|
- тесты: `tests/test_regress.py` — 16 проверок на те же баги, что ловило ревью; CI: `.github/workflows/ci.yml` (py3.10/3.11/3.12)
|
||||||
@@ -51,10 +51,56 @@ python -m omni.bot
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m omni serve --port 8077
|
python -m omni serve --port 8077
|
||||||
# http://127.0.0.1:8077/?view=abuse
|
# http://127.0.0.1:8077/#/abuse
|
||||||
```
|
```
|
||||||
|
|
||||||
`?view=feed|src|runs|abuse`, горячие клавиши `1..4`, палитра `Ctrl K`, категории в URL (`?cat=`), кнопка «авто-добавление» ищет новые фиды на уже подключённых хостах и сохраняет только те, что реально отдали записи.
|
Маршруты через hash: `#/feed` · `#/abuse` · `#/sources` · `#/tg` · `#/watch` · `#/digest` · `#/overview` · `#/runs` · `#/keys`, горячие клавиши `1..4`, палитра `Ctrl K`, категории в URL (`?cat=`), кнопка «авто-добавление» ищет новые фиды на уже подключённых хостах и сохраняет только те, что реально отдали записи.
|
||||||
|
|
||||||
|
## Скриншоты панели
|
||||||
|
|
||||||
|
Снято с живого инстанса (`python -m omni serve --port 8077`), база на момент съёмки: **46 423 записи · 3 815 источников · 6 746 находок**. Пересъёмка одной командой: `python tools/shots_ui.py` (нужен playwright, ждёт исчезновения скелетонов и только потом стреляет).
|
||||||
|
|
||||||
|
### Лента
|
||||||
|
|
||||||
|
Фильтры по периоду, категории и сортировке, чипы типов, экспорт CSV/JSON, читалка по клику.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Находки
|
||||||
|
|
||||||
|
Промо, триалы, сливы и панели с бесплатными API: 6 746 находок, 1 368 уникальных, фильтр по тегам (`ключи / промо / триалы / сливы / кредиты / инвайты / lifetime deal`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Источники
|
||||||
|
|
||||||
|
Реестр на 3 815 источников: тумблер вкл/выкл, результат проверки, число записей в базе, дата последней проверки, добавление своего фида и health-аудит по категории.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Telegram
|
||||||
|
|
||||||
|
Своя серверная сессия: поиск каналов по теме через Telegram и импорт выбранных публикаций в базу (глубина — 100 постов на канал).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Отслеживание и Дайджест
|
||||||
|
|
||||||
|
Правила watchlist по слову и типу записи (совпадения считаются с момента добавления правила) и Markdown-сводка за сутки/3 дня/неделю с экспортом.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Система: обзор, история сборов, ключи
|
||||||
|
|
||||||
|
Счётчики реестра и импорта, журнал прогонов с числом новых/дублей/пустых по каждому источнику, состояние пула API-ключей по сервисам — только готовность и остывание, без значений и фрагментов ключей.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Что умеет
|
## Что умеет
|
||||||
|
|
||||||
@@ -324,7 +370,7 @@ MIT.
|
|||||||
|
|
||||||
## Абузы, авто-добавление и Telegram-бот
|
## Абузы, авто-добавление и Telegram-бот
|
||||||
|
|
||||||
- **абузы**: категория `abuse` — 31 живая площадка (XenForo/Discourse/Telegram/RSS), включая свои каналы, классификатор `omni/abuse.py` раскладывает записи по тегам `ключи / промо / триалы / сливы / софт / продажа / новости`. В панели — отдельная вкладка (`?view=abuse`), в боте — `/abuse [тег]`.
|
- **абузы**: категория `abuse` — 31 живая площадка (XenForo/Discourse/Telegram/RSS), включая свои каналы, классификатор `omni/abuse.py` раскладывает записи по тегам `ключи / промо / триалы / сливы / софт / продажа / новости`. В панели — отдельный раздел `#/abuse`, в боте — `/abuse [тег]`.
|
||||||
- **авто-добавление**: `omni/discover.py` ищет фиды на уже подключённых хостах и каталогах, проверяет их теми же адаптерами и сохраняет только те, что реально отдали записи. В панели — кнопка «авто-добавление», в API — `POST /api/discover?hosts=40&add=25`.
|
- **авто-добавление**: `omni/discover.py` ищет фиды на уже подключённых хостах и каталогах, проверяет их теми же адаптерами и сохраняет только те, что реально отдали записи. В панели — кнопка «авто-добавление», в API — `POST /api/discover?hosts=40&add=25`.
|
||||||
- **новости**: любые RSS/Atom/JSON-ленты (редакции, релизы, сообщества) подключаются как обычный источник или через `/add <url>` в боте — отдельной схемы не требуется.
|
- **новости**: любые RSS/Atom/JSON-ленты (редакции, релизы, сообщества) подключаются как обычный источник или через `/add <url>` в боте — отдельной схемы не требуется.
|
||||||
|
|
||||||
|
|||||||
BIN
docs/screenshots/abuse.png
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
docs/screenshots/digest.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
docs/screenshots/feed.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
docs/screenshots/keys.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/screenshots/overview.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
docs/screenshots/runs.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
docs/screenshots/sources.png
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
docs/screenshots/tg.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
docs/screenshots/watch.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
78
tools/shots_ui.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""Пересъёмка скриншотов панели в 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()
|
||||||