Files
vesti/web/templates/sources.html
T
kpa39l 48c25f13d4 feat(sources): форма добавления источника — одно поле Address, авто-название из TG/RSS (address-field-enhancement)
- parse_address(address, crawler) в sources/sources.py: telegram (t.me/name, @name,
  https://t.me/s/name) → slug/channel/url; rss → feed_url/url/slug из домена.
- GET /sources/preview: telegram — название через Telethon get_entity (title),
  rss — через feedparser (feed.title); slug и name возвращаются в JSON.
- _form_source: единое поле address вместо url/channel/feed_url; slug из адреса.
- /sources: direction datalist из DIRECTIONS + уникальные значения из БД,
  свободный ввод остаётся.
- Форма: поле Address + кнопка «Найти» (JS fetch → заполнение slug/name),
  direction datalist динамический.

Также в sources.py входит незакоммиченная ранее веб-CRUD-логика (upsert_source_yaml,
delete_source_yaml, sync_yaml_to_db_and_back и др.) — работала на проде, теперь в git.
2026-09-16 17:03:23 +00:00

168 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">Address</label>
<input type="text" name="address" class="form-control form-control-sm"
placeholder="@handle, t.me/name, https://t.me/name или https://.../rss" id="add_address">
</div>
<div class="col-auto">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn_fetch"
onclick="fetchSourcePreview()">🔎 Найти</button>
</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">direction</label>
<input type="text" name="direction" class="form-control form-control-sm" placeholder="linux" list="dirs">
<datalist id="dirs">
{% for d in directions %}<option value="{{ d }}">{% endfor %}
</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 addAddress = document.getElementById('add_address');
const addSlug = document.querySelector('form[action="/sources/add"] input[name="slug"]');
const addName = document.querySelector('form[action="/sources/add"] input[name="name"]');
if (addCrawler && addAddress) {
function toggleAddress() {
addAddress.placeholder = addCrawler.value === 'telegram'
? '@handle, t.me/name, https://t.me/name'
: 'https://.../rss или http://...';
}
addCrawler.addEventListener('change', toggleAddress);
toggleAddress();
}
async function fetchSourcePreview() {
const addr = addAddress.value.trim();
if (!addr) { alert('Введите адрес'); return; }
try {
const resp = await fetch(`/sources/preview?address=${encodeURIComponent(addr)}&crawler=${addCrawler.value}`);
const data = await resp.json();
if (data.error) { alert(data.error); return; }
if (data.slug) addSlug.value = data.slug;
if (data.name) { addName.value = data.name; }
else if (addName.value === '') addName.placeholder = 'Название не найдено — введите вручную';
} catch (e) { alert('Ошибка запроса: ' + e); }
}
</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 %}