# Tasks: crawler-queue ## 1. Воркер (фаза 3) - [x] 1.1 crawler/worker.py: claim_post (атомарный claim: classified=0 → -1, BEGIN IMMEDIATE) Проверка: in-memory тест — два вызова claim_post дают разные id (атомарно) - [x] 1.2 process_post: classify_text + трафилатура для summary-only; UPDATE posts (direction/relevance/interest/summary/classified=1) Проверка: in-memory — пост «linux» → tech, classified=1; «игры» → games - [x] 1.3 Исключение → classified=0 (вернуть в очередь) Проверка: try/except в work_loop, UPDATE classified=0; реализовано - [x] 1.4 run(workers=8, limit=200): ThreadPoolExecutor, as_completed, счётчики (processed/classified/llm_ok) Проверка: `python -m crawler.worker --workers 4 --limit 2` → 8 обработано; без ошибок ## 2. Сбор (фаза 1) - [x] 2.1 crawler/crawl_sources.py: запуск telegram_crawler + rss_crawler (сбор новых кандидатов в очередь) Проверка: `python -m crawler.crawl_sources --all` → запускает оба; `--crawler rss` — только RSS ## 3. Страница /crawlers - [x] 3.1 web/app.py: GET /crawlers (за auth) — таблица источников (slug, name, crawler, status, last_fetch, last_error, error_count, priority) + карточки очереди (pending/processing/done) Проверка: GET /crawlers с auth → 200, таблица с lwn (проверено httpx) - [x] 3.2 web/templates/crawlers.html — шаблон (таблица + карточки + кнопки) - [x] 3.3 POST /crawlers/run → запуск worker.run (фаза 3); POST /crawlers/reset → sources.status='alive', error_count=0 Проверка: POST /crawlers/run → воркер реально стартует (logs/worker.log), 200/302; reset — UPDATE ## 4. Интеграция и доки - [x] 4.1 AGENT.MD — команды worker/crawl_sources (добавлены) - [x] 4.2 STATUS.md / TODO.md — страница /crawlers, параллельная фаза 3 (обновлены) - [x] 4.3 `openspec validate crawler-queue` → 0 ошибок - [x] 4.4 TODO.md: задача «crawler-очередь» закрыта ✅ ## Примечания (найденные при реализации) - SQLite: соединение привязано к потоку — создаётся ВНУТРИ work_loop (нельзя делить между потоками). - Локальная Ollama (qwen3:8b) держит 1 слот — 8 параллельных LLM-запросов → таймауты (посты возвращаются в очередь, не теряются). Добавлен `LLM_SEM` (threading.BoundedSemaphore, MAX_CONCURRENT_LLM=2). - Очередь = posts (classified IS NULL OR 0) — отдельная таблица НЕ нужна (дублирование). - error_count/etag для RSS — из rss_state (в sources их нет); запрос /crawlers использует COALESCE. - **SOURCE_RULES (classifier/keywords.py)**: правило источника с приоритетом над словарём и LLM. `lwn → tech` — все посты LWN (технологическое СМИ) получают направление tech детерминированно, без LLM (method='source-rule'). Работает в worker.py и старом CLI classifier.classify (оба JOIN sources → source_slug).