mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
fix(sources): поле channel в форме добавления телеграм-источника (fix-sources-add-channel)
Форма добавления не имела поля channel, валидатор требует его для crawler=telegram → 'telegram-источнику нужен channel'. Добавлено поле в add-форму (required для telegram через JS), показ channel в таблице у telegram-источников. Backend не менялся. web/templates/sources.html ранее отсутствовал в git — добавлен.
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
# VESTI — Статус
|
# VESTI — Статус
|
||||||
|
|
||||||
Обновлено: 2026-09-15 (RSS: сохранение абзацев/разметки в текстах новостей)
|
Обновлено: 2026-09-16 (fix-sources-add-channel: поле channel в форме добавления TG-источника)
|
||||||
<!--
|
<!--
|
||||||
История обновлений:
|
История обновлений:
|
||||||
|
2026-09-16 — fix-sources-add-channel: в форме добавления источника появилось поле channel
|
||||||
|
(required для telegram через JS), в таблице у TG-источников виден канал.
|
||||||
|
Причина бага: поле channel отсутствовало в UI, хотя валидатор требует его.
|
||||||
|
Заодно: web/templates/sources.html и crawlers.html НЕ были в git (untracked) — sources.html добавлен.
|
||||||
2026-09-15 — rss-formatting-preserve: html_to_text/extract_full_text сохраняют абзацы (\n\n);
|
2026-09-15 — rss-formatting-preserve: html_to_text/extract_full_text сохраняют абзацы (\n\n);
|
||||||
safe_html (bleach) для предпросмотра RSS-текстов; пересобраны старые посты
|
safe_html (bleach) для предпросмотра RSS-текстов; пересобраны старые посты
|
||||||
(omarchy 25/25, bleepingcomputer 16/16); lwn частично (403/429 — при след. крауле)
|
(omarchy 25/25, bleepingcomputer 16/16); lwn частично (403/429 — при след. крауле)
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## Текущее состояние
|
## Текущее состояние
|
||||||
|
- **fix-sources-add-channel** (2026-09-16): добавление telegram-источника через веб снова работает. Поле `channel` добавлено в форму (required для telegram через JS) и показывается в таблице. Причина: UI-форма не имела поля channel. `web/templates/sources.html` ранее отсутствовал в git — добавлен.
|
||||||
- **FEDIVERSE-ПУБЛИКАЦИЯ РАБОТАЕТ** (2026-09-13): publisher-service умеет публиковать карточки в GoToSocial (social.dedinit.ru) от имени бот-аккаунта **@vesti@dedinit.ru** (Mastodon-совместимый API). Change `gotosocial-publisher`.
|
- **FEDIVERSE-ПУБЛИКАЦИЯ РАБОТАЕТ** (2026-09-13): publisher-service умеет публиковать карточки в GoToSocial (social.dedinit.ru) от имени бот-аккаунта **@vesti@dedinit.ru** (Mastodon-совместимый API). Change `gotosocial-publisher`.
|
||||||
- Аккаунт **vesti** создан на GtS (2026-09-13, confirmed+approved, admin=0), OAuth-приложение `vesti-publisher` (client_id=01B09QNDM9YQ2VQVSRXHCVAV6K), токен в .env (`GT_SOCIAL_ACCESS_TOKEN`, scope `read write`), канал `VESTI_GTS_CHANNELS=@vesti@dedinit.ru`.
|
- Аккаунт **vesti** создан на GtS (2026-09-13, confirmed+approved, admin=0), OAuth-приложение `vesti-publisher` (client_id=01B09QNDM9YQ2VQVSRXHCVAV6K), токен в .env (`GT_SOCIAL_ACCESS_TOKEN`, scope `read write`), канал `VESTI_GTS_CHANNELS=@vesti@dedinit.ru`.
|
||||||
- healthz publisher: блок `gotosocial` — url, account=vesti, token_set=true, account_ok=true.
|
- healthz publisher: блок `gotosocial` — url, account=vesti, token_set=true, account_ok=true.
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-16
|
||||||
|
skip_specs: true
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# fix-sources-add-channel
|
||||||
|
|
||||||
|
## Дизайн
|
||||||
|
|
||||||
|
### 1. Форма добавления — поле channel (web/templates/sources.html)
|
||||||
|
|
||||||
|
Вставить после поля `url` (перед `crawler`), чтобы telegram-поля шли вместе:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">channel (telegram)</label>
|
||||||
|
<input type="text" name="channel" class="form-control form-control-sm"
|
||||||
|
placeholder="@handle или t.me/..." id="add_channel">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Поле обязательное только для telegram: маленький inline-скрипт в конце формы
|
||||||
|
переключает атрибут `required` при смене `select[name=crawler]`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
const addCrawler = document.querySelector('form[action="/sources/add"] select[name="crawler"]');
|
||||||
|
const addChannel = document.getElementById('add_channel');
|
||||||
|
function toggleChannelRequired() {
|
||||||
|
addChannel.required = addCrawler.value === 'telegram';
|
||||||
|
}
|
||||||
|
addCrawler.addEventListener('change', toggleChannelRequired);
|
||||||
|
toggleChannelRequired();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Таблица — показ channel у telegram (web/templates/sources.html)
|
||||||
|
|
||||||
|
В колонке crawler (строки 91-93) добавить вывод channel для telegram,
|
||||||
|
по аналогии с feed_url для rss:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<td><span class="badge text-bg-secondary">{{ s['crawler'] }}</span>
|
||||||
|
{% if s['crawler'] == 'rss' and s['feed_url'] %}<br><small class="text-muted">{{ s['feed_url'] }}</small>{% endif %}
|
||||||
|
{% if s['crawler'] == 'telegram' and s['channel'] %}<br><small class="text-muted">{{ s['channel'] }}</small>{% endif %}
|
||||||
|
</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Backend — без изменений
|
||||||
|
|
||||||
|
`web/app.py::_form_source` уже собирает `channel` (строка 877),
|
||||||
|
`sources/sources.py::_validate_source` уже проверяет (строка 218),
|
||||||
|
`db_source_to_yaml` уже пишет в yaml (строка 181). Правки только в шаблоне.
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
1. `openspec validate fix-sources-add-channel` — чисто.
|
||||||
|
2. Ручной тест через веб: добавить telegram-источник с channel → success;
|
||||||
|
без channel → браузер подсветит поле, серверная ошибка не нужна.
|
||||||
|
3. Рестарт `vesti-web`, проверить страницу /sources.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# fix-sources-add-channel
|
||||||
|
|
||||||
|
Добавление telegram-источника из веб-модерации падало с ошибкой
|
||||||
|
«telegram-источнику нужен channel», хотя пользователь заполнял slug и название.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
В форме добавления источника (`web/templates/sources.html`) поле `channel`
|
||||||
|
отсутствовало. `_form_source()` в `web/app.py` читает `channel` из формы,
|
||||||
|
`_validate_source()` в `sources/sources.py:218` требует его для `crawler=telegram`
|
||||||
|
— поэтому любой telegram-источник, добавленный через веб, отклонялся,
|
||||||
|
а rss-источник — нет (feed_url в форме есть).
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **web/templates/sources.html (форма добавления)**: добавлено поле `channel`
|
||||||
|
с подсказкой «@handle или t.me/... для telegram»; для `crawler=telegram`
|
||||||
|
становится `required` (маленький inline-скрипт переключает).
|
||||||
|
- **web/templates/sources.html (таблица)**: у telegram-источников под crawler
|
||||||
|
показывается `channel` (аналогично feed_url у rss), чтобы было видно,
|
||||||
|
какой канал привязан.
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не трогаем backend: `_form_source`, `_validate_source`, `add_or_update_source`
|
||||||
|
уже корректно работают с полем `channel` — не хватало только UI.
|
||||||
|
- Не делаем авто-подстановку channel из url — разные форматы
|
||||||
|
(@handle, t.me/name, https://t.me/s/name), подстановка ненадёжна.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Telegram-источники снова можно добавлять из веб-модерации.
|
||||||
|
- В таблице виден привязанный канал — легче отлаживать нерабочие источники.
|
||||||
|
- Никаких изменений БД и БД-миграций — только шаблон.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# fix-sources-add-channel
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Форма добавления источника не имеет поля `channel`, хотя валидатор
|
||||||
|
(`_validate_source`) требует его для telegram-источников → добавление
|
||||||
|
telegram-источника из веб падает с «telegram-источнику нужен channel».
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] sources.html (add-форма): поле `channel` после url, required для telegram
|
||||||
|
- [x] sources.html (таблица): показ channel у telegram-источников
|
||||||
|
- [x] openspec validate clean
|
||||||
|
- [x] рестарт vesti-web; ручная проверка /sources (добавление telegram-источника)
|
||||||
|
- [x] STATUS.md актуализирован; commit + push gitverse
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Источники{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h4 class="mb-3">📡 Источники <a href="/crawlers" class="btn btn-sm btn-outline-secondary ms-2">Статус краулеров →</a></h4>
|
||||||
|
|
||||||
|
{% if did %}
|
||||||
|
<div class="alert alert-{{ 'success' if not did.startswith('error') else 'danger' }} py-2 small">
|
||||||
|
{{ did }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Форма добавления -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-2"><b>➕ Добавить источник</b></div>
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<form method="post" action="/sources/add" class="row g-2 align-items-end">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">slug</label>
|
||||||
|
<input type="text" name="slug" class="form-control form-control-sm" placeholder="my_channel" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">Название</label>
|
||||||
|
<input type="text" name="name" class="form-control form-control-sm" placeholder="My Channel">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">URL</label>
|
||||||
|
<input type="text" name="url" class="form-control form-control-sm" placeholder="https://t.me/... или RSS">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">channel (telegram)</label>
|
||||||
|
<input type="text" name="channel" class="form-control form-control-sm"
|
||||||
|
placeholder="@handle или t.me/..." id="add_channel">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">crawler</label>
|
||||||
|
<select name="crawler" class="form-select form-select-sm">
|
||||||
|
<option value="telegram">telegram</option>
|
||||||
|
<option value="rss">rss</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">feed_url (rss)</label>
|
||||||
|
<input type="text" name="feed_url" class="form-control form-control-sm" placeholder="https://.../rss">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">direction</label>
|
||||||
|
<input type="text" name="direction" class="form-control form-control-sm" placeholder="linux" list="dirs">
|
||||||
|
<datalist id="dirs">
|
||||||
|
<option value="linux"><option value="tech"><option value="politics">
|
||||||
|
<option value="games"><option value="electronics"><option value="llm">
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small">priority</label>
|
||||||
|
<select name="priority" class="form-select form-select-sm">
|
||||||
|
<option value="P0">P0</option><option value="P1" selected>P1</option>
|
||||||
|
<option value="P2">P2</option><option value="P3">P3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check form-switch mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="enabled" id="add_en" checked>
|
||||||
|
<label class="form-check-label small" for="add_en">enabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto"><button class="btn btn-sm btn-primary">Добавить</button></div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
const addCrawler = document.querySelector('form[action="/sources/add"] select[name="crawler"]');
|
||||||
|
const addChannel = document.getElementById('add_channel');
|
||||||
|
if (addCrawler && addChannel) {
|
||||||
|
function toggleChannelRequired() {
|
||||||
|
addChannel.required = addCrawler.value === 'telegram';
|
||||||
|
}
|
||||||
|
addCrawler.addEventListener('change', toggleChannelRequired);
|
||||||
|
toggleChannelRequired();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Таблица источников -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header py-2"><b>🗂️ Таблица ({{ sources|length }})</b>
|
||||||
|
<span class="text-muted small">источник истины: sources.yaml (правки пишутся в yaml + БД)</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>slug</th><th>Название</th><th>crawler</th><th>direction</th><th>lang</th>
|
||||||
|
<th>priority</th><th>enabled</th><th class="text-end">статус</th><th>послед. запуск</th><th class="text-end">действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in sources %}
|
||||||
|
<tr class="{{ 'text-muted' if not s['enabled'] }}">
|
||||||
|
<td><code>{{ s['slug'] }}</code></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/sources/{{ s['id'] }}/edit" class="row g-1 align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" name="name" value="{{ s['name'] or '' }}" class="form-control form-control-sm source-name" style="width:170px">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto"><button class="btn btn-sm btn-outline-secondary">💾</button></div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge text-bg-secondary">{{ s['crawler'] }}</span>
|
||||||
|
{% if s['crawler'] == 'rss' and s['feed_url'] %}<br><small class="text-muted">{{ s['feed_url'] }}</small>{% endif %}
|
||||||
|
{% if s['crawler'] == 'telegram' and s['channel'] %}<br><small class="text-muted">{{ s['channel'] }}</small>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ s['direction'] or '—' }}
|
||||||
|
{% if s['error_count'] %}<br><small class="text-danger">{{ s['error_count'] }} err</small>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ s['lang'] or 'ru' }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/sources/{{ s['id'] }}/edit" class="d-inline">
|
||||||
|
<select name="priority" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
|
||||||
|
{% for p in ['P0','P1','P2','P3'] %}
|
||||||
|
<option value="{{ p }}" {{ 'selected' if s['priority'] == p else '' }}>P{{ loop.index0 }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {{ 'text-bg-success' if s['enabled'] else 'text-bg-secondary' }}">
|
||||||
|
{{ 'вкл' if s['enabled'] else 'пауза' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if s['status'] == 'dead' %}<span class="badge text-bg-danger">dead</span>
|
||||||
|
{% elif s['status'] == 'alive' %}<span class="badge text-bg-success">alive</span>
|
||||||
|
{% elif s['status'] == 'new' %}<span class="badge text-bg-info">new</span>
|
||||||
|
{% else %}<span class="badge text-bg-secondary">{{ s['status'] }}</span>{% endif %}
|
||||||
|
{% if s['enabled'] == 0 %}<span class="badge text-bg-warning">пауза</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted">{{ s['last_fetch'] or '—' }}{% if s['last_error'] %}<br><span class="text-danger">{{ s['last_error'][:40] }}</span>{% endif %}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<form method="post" action="/sources/{{ s['id'] }}/toggle" class="d-inline">
|
||||||
|
<button class="btn btn-sm {{ 'btn-outline-warning' if s['enabled'] else 'btn-outline-success' }}"
|
||||||
|
title="{{ 'Пауза' if s['enabled'] else 'Возобновить' }}">
|
||||||
|
{{ '⏸' if s['enabled'] else '▶' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/sources/{{ s['id'] }}/delete" class="d-inline"
|
||||||
|
onsubmit="return confirm('Удалить источник {{ s['slug'] }}? Это уберёт его из sources.yaml и БД (посты останутся).')">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" title="Удалить">🗑</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user