fix: address user feedback on sources, raw_posts and workers UI/UX

This commit is contained in:
Your Name
2026-07-18 23:28:02 +05:00
parent 8bca008f74
commit 89a7ff7723
6 changed files with 205 additions and 232 deletions
+5 -3
View File
@@ -1966,22 +1966,23 @@ async def logout(request: Request):
@app.get("/sources", response_class=HTMLResponse) @app.get("/sources", response_class=HTMLResponse)
async def sources_list(request: Request, q: str = "", status_filter: str = "", page: int = 1, per_page: int = 50): async def sources_list(request: Request, q: str = "", status_filter: str = "", active_filter: str = "", page: int = 1, per_page: int = 50):
user = await get_current_user(request) user = await get_current_user(request)
if not user: if not user:
return redirect("/login") return redirect("/login")
pool = await get_pool() pool = await get_pool()
table_filters = parse_table_filters(request, SOURCE_FIELD_SPECS) table_filters = parse_table_filters(request, SOURCE_FIELD_SPECS)
table_sorts = parse_table_sorts(request, SOURCE_FIELD_SPECS) table_sorts = parse_table_sorts(request, SOURCE_FIELD_SPECS)
filter_clauses, filter_args = build_filter_sql(table_filters, SOURCE_FIELD_SPECS, 3) filter_clauses, filter_args = build_filter_sql(table_filters, SOURCE_FIELD_SPECS, 4)
where_parts = [ where_parts = [
"s.archived_at IS NULL", "s.archived_at IS NULL",
"($1='' OR s.name ILIKE '%' || $1 || '%' OR COALESCE(s.tag,'') ILIKE '%' || $1 || '%' OR s.url ILIKE '%' || $1 || '%' OR COALESCE(s.external_id,'') ILIKE '%' || $1 || '%')", "($1='' OR s.name ILIKE '%' || $1 || '%' OR COALESCE(s.tag,'') ILIKE '%' || $1 || '%' OR s.url ILIKE '%' || $1 || '%' OR COALESCE(s.external_id,'') ILIKE '%' || $1 || '%')",
"($2='' OR s.status=$2)", "($2='' OR s.status=$2)",
"($3='' OR ($3='on' AND s.active=TRUE) OR ($3='off' AND s.active=FALSE))",
*filter_clauses, *filter_clauses,
] ]
where_sql = " AND ".join(where_parts) where_sql = " AND ".join(where_parts)
base_args = [q.strip(), status_filter.strip(), *filter_args] base_args = [q.strip(), status_filter.strip(), active_filter.strip(), *filter_args]
total = int( total = int(
await pool.fetchval( await pool.fetchval(
f""" f"""
@@ -2029,6 +2030,7 @@ async def sources_list(request: Request, q: str = "", status_filter: str = "", p
sources=sources, sources=sources,
q=q, q=q,
status_filter=status_filter, status_filter=status_filter,
active_filter=active_filter,
pagination=pager, pagination=pager,
table_filters=table_filters, table_filters=table_filters,
table_sorts=table_sorts, table_sorts=table_sorts,
@@ -2,7 +2,7 @@
<!-- Sidebar Filters --> <!-- Sidebar Filters -->
<aside class="w-full xl:w-72 flex-shrink-0"> <aside class="w-full xl:w-72 flex-shrink-0">
<div class="card p-4 sticky top-6"> <div class="card p-4 sticky top-6">
<form id="filter-form" hx-get="/raw" hx-target="#content-area" hx-push-url="true" hx-trigger="submit, change delay:300ms"> <form id="filter-form" hx-get="/raw" hx-target="#content-area" hx-push-url="true" hx-trigger="submit">
<input type="hidden" name="sort" value="{{ sort }}"> <input type="hidden" name="sort" value="{{ sort }}">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
@@ -32,7 +32,7 @@
<!-- Qualification Status --> <!-- Qualification Status -->
<div> <div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Квалификация</label> <label class="block text-xs font-semibold text-app-textMain mb-2">Квалификация</label>
<div class="bg-app-bg border border-app-border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1"> <div class="bg-app-bg border border-app-border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1 custom-scrollbar">
{% for item in facets.qualification_statuses %} {% for item in facets.qualification_statuses %}
<label class="flex items-center gap-2 px-2 py-1 hover:bg-app-surfaceHover rounded cursor-pointer transition-colors group"> <label class="flex items-center gap-2 px-2 py-1 hover:bg-app-surfaceHover rounded cursor-pointer transition-colors group">
<input type="checkbox" name="qualification_status" value="{{ item.value }}" class="checkbox" {% if item.value in qualification_statuses %}checked{% endif %} /> <input type="checkbox" name="qualification_status" value="{{ item.value }}" class="checkbox" {% if item.value in qualification_statuses %}checked{% endif %} />
@@ -46,7 +46,7 @@
<!-- Rewrite Status --> <!-- Rewrite Status -->
<div> <div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Рерайт</label> <label class="block text-xs font-semibold text-app-textMain mb-2">Рерайт</label>
<div class="bg-app-bg border border-app-border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1"> <div class="bg-app-bg border border-app-border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1 custom-scrollbar">
{% for item in facets.rewrite_statuses %} {% for item in facets.rewrite_statuses %}
<label class="flex items-center gap-2 px-2 py-1 hover:bg-app-surfaceHover rounded cursor-pointer transition-colors group"> <label class="flex items-center gap-2 px-2 py-1 hover:bg-app-surfaceHover rounded cursor-pointer transition-colors group">
<input type="checkbox" name="rewrite_status" value="{{ item.value }}" class="checkbox" {% if item.value in rewrite_statuses %}checked{% endif %} /> <input type="checkbox" name="rewrite_status" value="{{ item.value }}" class="checkbox" {% if item.value in rewrite_statuses %}checked{% endif %} />
@@ -56,6 +56,20 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<!-- Source -->
<div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Источник</label>
<div class="bg-app-bg border border-app-border rounded-lg p-2 max-h-48 overflow-y-auto space-y-1 custom-scrollbar">
{% for item in facets.sources %}
<label class="flex items-center gap-2 px-2 py-1 hover:bg-app-surfaceHover rounded cursor-pointer transition-colors group">
<input type="checkbox" name="source_id" value="{{ item.id }}" class="checkbox" {% if item.id in source_ids %}checked{% endif %} />
<span class="text-xs text-app-textMain flex-1 truncate group-hover:text-white" title="{{ item.name }}">{{ item.name }}</span>
<span class="badge badge-neutral bg-transparent border-none text-[10px]">{{ item.count }}</span>
</label>
{% endfor %}
</div>
</div>
<button class="btn btn-primary w-full" type="submit">Применить фильтры</button> <button class="btn btn-primary w-full" type="submit">Применить фильтры</button>
</div> </div>
@@ -72,7 +86,7 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm text-app-textMuted">На странице:</span> <span class="text-sm text-app-textMuted">На странице:</span>
<select name="per_page" class="select select-sm w-auto py-1" form="filter-form"> <select name="per_page" class="select select-sm w-auto py-1" form="filter-form" onchange="document.getElementById('filter-form').dispatchEvent(new Event('submit', {cancelable: true}))">
{% for n in [25,50,100,200] %} {% for n in [25,50,100,200] %}
<option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option> <option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %} {% endfor %}
@@ -99,56 +113,54 @@
<a href="/raw/{{ p.id }}" class="text-app-primary hover:text-white transition-colors">#{{ p.id }}</a> <a href="/raw/{{ p.id }}" class="text-app-primary hover:text-white transition-colors">#{{ p.id }}</a>
</td> </td>
<td class="px-4 py-3 whitespace-normal min-w-[200px]"> <td class="px-4 py-3 whitespace-normal min-w-[200px]">
<a href="{{ p.original_url }}" target="_blank" class="font-semibold text-app-textMain group-hover:text-app-primary transition-colors">{{ p.source_name }}</a> <a href="{{ p.original_url }}" target="_blank" class="font-semibold text-app-textMain hover:text-app-primary transition-colors">{{ p.source_name }}</a>
<div class="flex items-center gap-2 mt-1"> <div class="flex items-center gap-2 mt-1">
<span class="text-[10px] text-app-textMuted font-mono bg-app-bg px-1 rounded">{{ p.source_platform }}</span> <span class="text-[10px] text-app-textMuted font-mono bg-app-bg px-1 rounded">{{ p.source_platform }}</span>
<span class="text-[10px] text-blue-400 font-mono">#{{ p.source_tag or "—" }}</span> <span class="text-[10px] text-blue-400 font-mono">#{{ p.source_tag or "—" }}</span>
</div> </div>
{% if p.posted_at_fmt %}
<div class="text-[10px] text-app-textMuted mt-1">{{ p.posted_at_fmt }}</div>
{% endif %}
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium border <div class="flex flex-col items-start gap-1">
{% if p.stage.class == 'ok' %}bg-app-successBg text-app-success border-app-success/20 <span class="badge badge-neutral text-[9px] uppercase tracking-wider">{{ p.status }}</span>
{% elif p.stage.class == 'bad' %}bg-app-errorBg text-app-error border-app-error/20 {% if p.qualification_status %}
{% elif p.stage.class == 'warn' %}bg-app-warningBg text-app-warning border-app-warning/20 <span class="badge badge-success text-[9px] uppercase tracking-wider bg-app-success/10 text-app-success border-app-success/20">Квалиф: {{ p.qualification_status }}</span>
{% else %}bg-app-bg text-app-textMuted border-app-border{% endif %}"> {% endif %}
{{ p.stage.label }} {% if p.rewrite_status %}
<span class="badge badge-warning text-[9px] uppercase tracking-wider bg-app-warning/10 text-app-warning border-app-warning/20">Рерайт: {{ p.rewrite_status }}</span>
{% endif %}
</div> </div>
{% if p.error_reason %} {% if p.error %}
<div class="text-[10px] text-app-error mt-1 max-w-[200px] whitespace-normal leading-tight">{{ p.error_reason }}</div> <div class="text-[10px] text-app-error mt-1 max-w-[200px] whitespace-normal leading-tight">{{ p.error }}</div>
{% endif %} {% endif %}
</td> </td>
<td class="px-4 py-3 whitespace-normal w-full max-w-sm lg:max-w-xl xl:max-w-2xl"> <td class="px-4 py-3 whitespace-normal w-full max-w-sm lg:max-w-xl xl:max-w-2xl">
<div class="text-xs text-app-textMain/80 leading-relaxed font-sans line-clamp-3"> <div class="text-xs text-app-textMain/80 leading-relaxed font-sans line-clamp-3">
{{ p.raw_text }} {{ p.raw_text }}
</div> </div>
{% if p.posted_at_fmt %}
<div class="text-[10px] text-app-textMuted mt-1">Опубликован: {{ p.posted_at_fmt }}</div>
{% else %}
<div class="text-[10px] text-app-textMuted mt-1">Создан: {{ p.created_at_fmt }}</div>
{% endif %}
</td> </td>
<td class="px-4 py-3 text-center"> <td class="px-4 py-3 text-center">
<div class="flex flex-col items-center gap-1"> {% if p.qualification_score is not none %}
<span class="text-lg font-bold font-mono <div class="font-mono font-bold text-lg {% if p.qualification_score >= 8 %}text-app-success{% elif p.qualification_score >= 5 %}text-app-warning{% else %}text-app-error{% endif %}">{{ p.qualification_score }}</div>
{% if p.ai_badge.class == 'ok' %}text-app-success {% else %}
{% elif p.ai_badge.class == 'bad' %}text-app-error <span class="text-app-textMuted"></span>
{% elif p.ai_badge.class == 'warn' %}text-app-warning {% endif %}
{% else %}text-app-textMuted{% endif %}">
{{ p.ai_badge.label }}
</span>
<span class="text-[10px] text-app-textMuted font-medium tracking-wider uppercase">{{ p.ai_badge.sublabel }}</span>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% else %}
{% if not posts %}
<tr> <tr>
<td colspan="5" class="px-4 py-12 text-center"> <td colspan="5" class="px-4 py-12">
<div class="flex flex-col items-center justify-center text-app-textMuted"> <div class="flex flex-col items-center justify-center text-app-textMuted">
<i data-lucide="inbox" class="w-12 h-12 mb-3 text-app-borderFocus"></i> <i data-lucide="inbox" class="w-12 h-12 mb-3 text-app-borderFocus"></i>
<p class="text-sm">Нет данных по заданным фильтрам</p> <p class="text-sm">Нет данных по заданным фильтрам</p>
</div> </div>
</td> </td>
</tr> </tr>
{% endif %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -158,15 +170,15 @@
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="flex justify-center mt-4"> <div class="flex justify-center mt-4">
<div class="tabs p-1"> <div class="tabs p-1">
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="filter-form" name="page" value="1" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevrons-left" class="w-4 h-4"></i></button> <button hx-get="?page=1" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevrons-left" class="w-4 h-4"></i></button>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="filter-form" name="page" value="{{ pagination.page - 1 }}" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevron-left" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.page - 1 }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevron-left" class="w-4 h-4"></i></button>
<span class="flex items-center justify-center px-4 text-sm font-medium text-app-textMuted bg-app-bg rounded-md border border-app-border mx-1"> <span class="flex items-center justify-center px-4 text-sm font-medium text-app-textMuted bg-app-bg rounded-md border border-app-border mx-1">
{{ pagination.page }} из {{ pagination.pages }} {{ pagination.page }} из {{ pagination.pages }}
</span> </span>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="filter-form" name="page" value="{{ pagination.page + 1 }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevron-right" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.page + 1 }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevron-right" class="w-4 h-4"></i></button>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="filter-form" name="page" value="{{ pagination.pages }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevrons-right" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.pages }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevrons-right" class="w-4 h-4"></i></button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
+18 -9
View File
@@ -4,11 +4,6 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-bold text-app-textMain">{{ s.name }}</span> <span class="font-bold text-app-textMain">{{ s.name }}</span>
{% if s.active %}
<span class="badge badge-success px-1.5 py-0.5 text-[10px]" title="Включен">ON</span>
{% else %}
<span class="badge badge-error px-1.5 py-0.5 text-[10px]" title="Выключен">OFF</span>
{% endif %}
</div> </div>
<div class="text-[10px] font-mono text-app-textMuted mt-1" title="External ID">{{ s.external_id or "" }}</div> <div class="text-[10px] font-mono text-app-textMuted mt-1" title="External ID">{{ s.external_id or "" }}</div>
</td> </td>
@@ -18,9 +13,19 @@
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-col gap-1 items-start"> <div class="flex flex-col gap-1 items-start">
<span class="badge {% if s.status == 'ok' %}badge-success{% elif s.status == 'error' %}badge-error{% else %}badge-neutral{% endif %} px-1.5 py-0.5 text-[10px] uppercase tracking-wider">{{ s.status }}</span> <div class="flex items-center gap-2">
{% if s.active %}
<span class="flex items-center gap-1 text-[10px] font-medium text-app-success"><span class="w-1.5 h-1.5 rounded-full bg-app-success"></span> Работает</span>
{% else %}
<span class="flex items-center gap-1 text-[10px] font-medium text-app-error"><span class="w-1.5 h-1.5 rounded-full bg-app-error"></span> На паузе</span>
{% endif %}
</div>
<div class="flex items-center gap-1 mt-0.5">
<span class="text-[9px] text-app-textMuted uppercase tracking-wider">Парсер:</span>
<span class="badge {% if s.status == 'ok' %}bg-app-success/10 text-app-success border-app-success/20{% elif s.status == 'error' %}bg-app-error/10 text-app-error border-app-error/20{% else %}bg-app-bg text-app-textMuted border-app-border{% endif %} px-1.5 py-0.5 text-[9px] uppercase tracking-wider">{{ s.status }}</span>
</div>
{% if s.status_msg %} {% if s.status_msg %}
<span class="text-[10px] text-app-textMuted truncate max-w-[10rem]" title="{{ s.status_msg }}">{{ s.status_msg }}</span> <span class="text-[9px] text-app-textMuted truncate max-w-[10rem] mt-0.5" title="{{ s.status_msg }}">{{ s.status_msg }}</span>
{% endif %} {% endif %}
</div> </div>
</td> </td>
@@ -36,8 +41,12 @@
</a> </a>
<form hx-post="/sources/{{ s.id }}/toggle" hx-target="#source-row-{{ s.id }}" hx-swap="outerHTML" class="m-0"> <form hx-post="/sources/{{ s.id }}/toggle" hx-target="#source-row-{{ s.id }}" hx-swap="outerHTML" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-ghost btn-icon w-8 h-8 rounded-md hover:bg-app-surface text-app-textMuted hover:text-white" type="submit" title="{% if s.active %}Выключить{% else %}Включить{% endif %}"> <button class="btn btn-ghost btn-icon w-8 h-8 rounded-md hover:bg-app-surface text-app-textMuted hover:text-white" type="submit" title="{% if s.active %}Поставить на паузу{% else %}Возобновить{% endif %}">
<i data-lucide="power" class="w-4 h-4 {% if s.active %}text-app-success{% endif %}"></i> {% if s.active %}
<i data-lucide="pause-circle" class="w-4 h-4 text-app-warning"></i>
{% else %}
<i data-lucide="play-circle" class="w-4 h-4 text-app-success"></i>
{% endif %}
</button> </button>
</form> </form>
<form hx-post="/sources/{{ s.id }}/delete" hx-target="#source-row-{{ s.id }}" hx-swap="outerHTML swap:1s" hx-confirm="Удалить источник {{ s.name }}?" class="m-0"> <form hx-post="/sources/{{ s.id }}/delete" hx-target="#source-row-{{ s.id }}" hx-swap="outerHTML swap:1s" hx-confirm="Удалить источник {{ s.name }}?" class="m-0">
+51 -87
View File
@@ -20,69 +20,31 @@
</summary> </summary>
<div class="p-6 border-t border-app-border bg-app-bg/30"> <div class="p-6 border-t border-app-border bg-app-bg/30">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-10"> <div class="max-w-2xl">
<form method="post" action="/sources/preview" class="flex flex-col gap-4">
<!-- Single Add --> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<div> <input type="hidden" name="q" value="{{ q }}">
<h3 class="font-bold text-white mb-4 flex items-center gap-2"><i data-lucide="file-plus" class="w-4 h-4 text-app-textMuted"></i> Добавить один</h3> <input type="hidden" name="status_filter" value="{{ status_filter }}">
<form method="post" action="/sources/new" class="flex flex-col gap-4"> <input type="hidden" name="active_filter" value="{{ active_filter }}">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="per_page" value="{{ pagination.per_page }}">
<input type="hidden" name="platform" value="vk">
<input type="hidden" name="priority" value="100"> <div>
<label class="block text-xs font-bold text-app-textMuted mb-1 flex justify-between">
<div> <span>Список (Название Тэг Ссылка)</span>
<label class="block text-xs font-bold text-app-textMuted mb-1">Название</label> <span class="text-[10px] text-app-textMuted/60 font-normal">По 1 на строку</span>
<input name="name" class="input" placeholder="Можно оставить пустым, подтянется из VK">
</div>
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Тэг (для хэштега)</label>
<input name="tag" class="input" placeholder="academy_gear">
</div>
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Ссылка VK <span class="text-app-error">*</span></label>
<input name="url" class="input" placeholder="https://vk.com/academy_gear" required>
</div>
<label class="flex items-center gap-3 mt-2 cursor-pointer group">
<input name="active" type="checkbox" class="checkbox" checked>
<span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Включить сразу после добавления</span>
</label> </label>
<textarea name="bulk_text" class="textarea font-mono text-xs h-40 leading-relaxed" placeholder="Academy Gear academy_gear https://vk.com/academy_gear&#10;A2 Technologies a2technologies https://vk.com/a2technologies">{{ bulk_text }}</textarea>
<button class="btn btn-primary mt-2 self-start" type="submit"> </div>
<i data-lucide="plus" class="w-4 h-4"></i> Добавить источник
</button> <label class="flex items-center gap-3 mt-2 cursor-pointer group">
</form> <input name="bulk_active" type="checkbox" class="checkbox" {% if bulk_active %}checked{% endif %}>
</div> <span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Включить после добавления</span>
</label>
<!-- Bulk Add -->
<div> <button class="btn btn-surface mt-2 self-start" type="submit">
<h3 class="font-bold text-white mb-4 flex items-center gap-2"><i data-lucide="list-plus" class="w-4 h-4 text-app-textMuted"></i> Массовое добавление</h3> <i data-lucide="search" class="w-4 h-4"></i> Проверить список
<form method="post" action="/sources/preview" class="flex flex-col gap-4"> </button>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> </form>
<input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="status_filter" value="{{ status_filter }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1 flex justify-between">
<span>Список (Название Тэг Ссылка)</span>
<span class="text-[10px] text-app-textMuted/60 font-normal">По 1 на строку</span>
</label>
<textarea name="bulk_text" class="textarea font-mono text-xs h-40 leading-relaxed" placeholder="Academy Gear academy_gear https://vk.com/academy_gear&#10;A2 Technologies a2technologies https://vk.com/a2technologies">{{ bulk_text }}</textarea>
</div>
<label class="flex items-center gap-3 mt-2 cursor-pointer group">
<input name="bulk_active" type="checkbox" class="checkbox" {% if bulk_active %}checked{% endif %}>
<span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Включить после добавления</span>
</label>
<button class="btn btn-surface mt-2 self-start" type="submit">
<i data-lucide="search" class="w-4 h-4"></i> Проверить список
</button>
</form>
</div>
</div> </div>
{% if source_preview %} {% if source_preview %}
@@ -143,34 +105,36 @@
<div class="card mb-6"> <div class="card mb-6">
<div class="p-4 border-b border-app-border bg-app-bg/50"> <div class="p-4 border-b border-app-border bg-app-bg/50">
<form class="flex flex-col md:flex-row gap-4 justify-between items-end" hx-get="/sources" hx-target="#content-area" hx-push-url="true"> <form id="filter-form" class="flex flex-wrap gap-4 items-end" hx-get="/sources" hx-target="#content-area" hx-push-url="true">
<div class="flex flex-col sm:flex-row gap-4 flex-1"> <div class="w-full sm:flex-1 min-w-[250px]">
<div class="w-full sm:w-72"> <label class="block text-xs font-bold text-app-textMuted mb-1">Поиск</label>
<label class="block text-xs font-bold text-app-textMuted mb-1">Поиск</label> <div class="relative">
<div class="relative"> <i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-app-textMuted"></i>
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-app-textMuted"></i> <input name="q" value="{{ q }}" placeholder="Название, ссылка, id" class="input w-full pl-9">
<input name="q" value="{{ q }}" placeholder="Название, ссылка, id" class="input w-full pl-9">
</div>
</div>
<div class="w-full sm:w-48">
<label class="block text-xs font-bold text-app-textMuted mb-1">Статус</label>
<select name="status_filter" class="select w-full">
<option value="">Все статусы</option>
{% for st in ["new","ok","paused","error"] %}
<option value="{{ st }}" {% if status_filter == st %}selected{% endif %}>{{ st }}</option>
{% endfor %}
</select>
</div>
<div class="w-full sm:w-auto">
<label class="block text-xs font-bold text-transparent mb-1 hidden sm:block">.</label>
<button class="btn btn-surface w-full sm:w-auto" type="submit">Найти</button>
</div> </div>
</div> </div>
<div class="flex items-center gap-2 mt-4 md:mt-0 w-full md:w-auto"> <div class="w-full sm:w-48">
{% include "table_tools.html" ignore missing %} <label class="block text-xs font-bold text-app-textMuted mb-1">Состояние</label>
<select name="active_filter" class="select w-full">
<option value="">Все состояния</option>
<option value="on" {% if active_filter == "on" %}selected{% endif %}>Включен (ON)</option>
<option value="off" {% if active_filter == "off" %}selected{% endif %}>Выключен (OFF)</option>
</select>
</div>
<div class="w-full sm:w-48">
<label class="block text-xs font-bold text-app-textMuted mb-1">Статус Парсера</label>
<select name="status_filter" class="select w-full">
<option value="">Все статусы</option>
{% for st in ["new","ok","paused","error"] %}
<option value="{{ st }}" {% if status_filter == st %}selected{% endif %}>{{ st }}</option>
{% endfor %}
</select>
</div>
<div class="w-full sm:w-auto">
<button class="btn btn-surface w-full sm:w-auto" type="submit">Найти</button>
</div> </div>
</form> </form>
</div> </div>
@@ -7,7 +7,7 @@
<th class="px-4 py-3">Название</th> <th class="px-4 py-3">Название</th>
<th class="px-4 py-3 w-32">Тэг</th> <th class="px-4 py-3 w-32">Тэг</th>
<th class="px-4 py-3 w-48">Ссылка</th> <th class="px-4 py-3 w-48">Ссылка</th>
<th class="px-4 py-3 w-32">Статус</th> <th class="px-4 py-3 w-32">Статус Парсера</th>
<th class="px-4 py-3 w-24 text-right">Посты</th> <th class="px-4 py-3 w-24 text-right">Посты</th>
<th class="px-4 py-3 w-40 text-right">Последний парсинг</th> <th class="px-4 py-3 w-40 text-right">Последний парсинг</th>
<th class="px-4 py-3 w-32 text-center">Действия</th> <th class="px-4 py-3 w-32 text-center">Действия</th>
@@ -34,15 +34,15 @@
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="flex justify-center p-4 border-t border-app-border"> <div class="flex justify-center p-4 border-t border-app-border">
<div class="tabs p-1"> <div class="tabs p-1">
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="pagination-form" name="page" value="1" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevrons-left" class="w-4 h-4"></i></button> <button hx-get="?page=1" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevrons-left" class="w-4 h-4"></i></button>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="pagination-form" name="page" value="{{ pagination.page - 1 }}" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevron-left" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.page - 1 }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page == 1 %}disabled{% endif %}><i data-lucide="chevron-left" class="w-4 h-4"></i></button>
<span class="flex items-center justify-center px-4 text-sm font-medium text-app-textMuted bg-app-bg rounded-md border border-app-border mx-1"> <span class="flex items-center justify-center px-4 text-sm font-medium text-app-textMuted bg-app-bg rounded-md border border-app-border mx-1">
{{ pagination.page }} из {{ pagination.pages }} {{ pagination.page }} из {{ pagination.pages }}
</span> </span>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="pagination-form" name="page" value="{{ pagination.page + 1 }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevron-right" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.page + 1 }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevron-right" class="w-4 h-4"></i></button>
<button class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" form="pagination-form" name="page" value="{{ pagination.pages }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevrons-right" class="w-4 h-4"></i></button> <button hx-get="?page={{ pagination.pages }}" hx-include="#filter-form" hx-target="#content-area" class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md" {% if pagination.page >= pagination.pages %}disabled{% endif %}><i data-lucide="chevrons-right" class="w-4 h-4"></i></button>
</div> </div>
</div> </div>
{% endif %} {% endif %}
+80 -94
View File
@@ -166,58 +166,51 @@
</div> </div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i> <i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary> </summary>
<div class="p-4 flex flex-col gap-4 bg-app-bg/30"> <div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<div class="text-xs text-app-textMuted leading-relaxed"> <div class="text-xs text-app-textMuted leading-relaxed">
Время по Екб. Нужны: воркер <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">tg-poster</span> и настройка <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">tg_poster_enabled</span>. Время по Екб. Нужны: воркер <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">tg-poster</span> и настройка <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">tg_poster_enabled</span>.
</div> </div>
<form method="post" action="/tg-poster-schedule/create" class="flex flex-wrap md:flex-nowrap items-end gap-3 bg-app-surface p-3 rounded-xl border border-app-border"> <!-- Active Schedule Chips -->
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <div class="flex flex-wrap gap-2">
<div class="flex-1 min-w-[120px]"> {% for row in tg_schedule %}
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время</label> {% if row.enabled %}
<input type="time" name="time" value="10:00" class="input input-sm w-full" required> <div class="inline-flex items-center gap-2 px-3 py-1.5 bg-app-surface border border-app-primary/30 rounded-lg shadow-sm group">
</div> <span class="font-mono text-sm font-bold text-white">{{ row.time[:2] }}:00</span>
<div class="w-20"> <span class="text-xs text-app-textMuted border-l border-app-border pl-2">{{ row.count }} шт.</span>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Постов</label> <form method="post" action="/tg-poster-schedule/{{ row.id }}/delete" class="m-0 ml-1 flex items-center">
<input type="number" name="count" value="1" min="1" max="20" class="input input-sm w-full" required> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</div> <button type="submit" class="text-app-textMuted hover:text-app-error transition-colors" title="Удалить">
<div class="flex items-center h-8 px-2 border border-app-border rounded-lg bg-app-bg"> <i data-lucide="x" class="w-3.5 h-3.5"></i>
<label class="cursor-pointer flex items-center gap-2 group"> </button>
<input type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" checked> </form>
<span class="text-xs font-medium text-app-textMain group-hover:text-white">Вкл</span> </div>
</label> {% endif %}
</div> {% else %}
<button class="btn btn-primary btn-sm h-8" type="submit">Добавить</button> <div class="text-sm text-app-textMuted italic py-2">Расписание пусто</div>
</form> {% endfor %}
<div class="overflow-x-auto border border-app-border rounded-xl">
<table class="w-full text-left text-xs whitespace-nowrap bg-app-surface">
<thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
<tr><th class="px-3 py-2">Время Екб</th><th class="px-3 py-2">Постов</th><th class="px-3 py-2">Статус</th><th class="px-3 py-2"></th></tr>
</thead>
<tbody class="divide-y divide-app-border">
{% for row in tg_schedule %}
<tr class="hover:bg-app-surfaceHover transition-colors">
<td class="px-3 py-2">
<form id="tg-schedule-update-{{ row.id }}" method="post" action="/tg-poster-schedule/{{ row.id }}/update" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</form>
<input form="tg-schedule-update-{{ row.id }}" type="time" name="time" value="{{ row.time }}" class="input input-sm w-full bg-app-bg h-7 text-white" required>
</td>
<td class="px-3 py-2"><input form="tg-schedule-update-{{ row.id }}" type="number" name="count" value="{{ row.count }}" min="1" max="20" class="input input-sm w-16 bg-app-bg h-7 text-white" required></td>
<td class="px-3 py-2"><label class="cursor-pointer flex justify-center"><input form="tg-schedule-update-{{ row.id }}" type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" {% if row.enabled %}checked{% endif %}></label></td>
<td class="px-3 py-2 flex gap-1 justify-center">
<button form="tg-schedule-update-{{ row.id }}" type="submit" class="btn btn-ghost btn-icon w-7 h-7 hover:text-app-success"><i data-lucide="check" class="w-4 h-4"></i></button>
<form method="post" action="/tg-poster-schedule/{{ row.id }}/delete" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="btn btn-ghost btn-icon w-7 h-7 text-app-textMuted hover:text-app-error hover:bg-app-errorBg"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<form method="post" action="/tg-poster-schedule/create" class="flex flex-wrap sm:flex-nowrap items-end gap-3 bg-app-surface p-4 rounded-xl border border-app-border">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="enabled" value="on">
<div class="w-full sm:w-32">
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время (час)</label>
<select name="time" class="select select-sm w-full font-mono">
{% for h in range(24) %}
{% set hh = "%02d" % h %}
<option value="{{ hh }}:00">{{ hh }}:00</option>
{% endfor %}
</select>
</div>
<div class="w-full sm:w-24">
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Кол-во</label>
<input type="number" name="count" value="1" min="1" max="20" class="input input-sm w-full text-center" required>
</div>
<button class="btn btn-primary btn-sm h-8 w-full sm:w-auto" type="submit">
<i data-lucide="plus" class="w-4 h-4"></i> Добавить
</button>
</form>
</div> </div>
</details> </details>
@@ -229,59 +222,52 @@
</div> </div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i> <i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary> </summary>
<div class="p-4 flex flex-col gap-4 bg-app-bg/30"> <div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<div class="text-xs text-app-textMuted leading-relaxed"> <div class="text-xs text-app-textMuted leading-relaxed">
Время по Екб. Нужны: воркер <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">vk-poster</span> и <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">vk_poster_enabled</span>. Время по Екб. Нужны: воркер <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">vk-poster</span> и <span class="font-mono bg-app-surface border border-app-border px-1.5 py-0.5 rounded text-[10px] text-white">vk_poster_enabled</span>.
</div> </div>
<div><a class="btn btn-surface btn-sm border-app-border" href="/vk-oauth/start"><i data-lucide="key" class="w-3.5 h-3.5"></i> Получить VK user token</a></div> <div><a class="btn btn-surface btn-sm border-app-border" href="/vk-oauth/start"><i data-lucide="key" class="w-3.5 h-3.5"></i> Получить VK user token</a></div>
<form method="post" action="/vk-poster-schedule/create" class="flex flex-wrap md:flex-nowrap items-end gap-3 bg-app-surface p-3 rounded-xl border border-app-border"> <!-- Active Schedule Chips -->
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <div class="flex flex-wrap gap-2">
<div class="flex-1 min-w-[120px]"> {% for row in vk_schedule %}
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время</label> {% if row.enabled %}
<input type="time" name="time" value="10:05" class="input input-sm w-full" required> <div class="inline-flex items-center gap-2 px-3 py-1.5 bg-app-surface border border-app-primary/30 rounded-lg shadow-sm group">
</div> <span class="font-mono text-sm font-bold text-white">{{ row.time[:2] }}:00</span>
<div class="w-20"> <span class="text-xs text-app-textMuted border-l border-app-border pl-2">{{ row.count }} шт.</span>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Постов</label> <form method="post" action="/vk-poster-schedule/{{ row.id }}/delete" class="m-0 ml-1 flex items-center">
<input type="number" name="count" value="1" min="1" max="20" class="input input-sm w-full" required> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</div> <button type="submit" class="text-app-textMuted hover:text-app-error transition-colors" title="Удалить">
<div class="flex items-center h-8 px-2 border border-app-border rounded-lg bg-app-bg"> <i data-lucide="x" class="w-3.5 h-3.5"></i>
<label class="cursor-pointer flex items-center gap-2 group"> </button>
<input type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" checked> </form>
<span class="text-xs font-medium text-app-textMain group-hover:text-white">Вкл</span> </div>
</label> {% endif %}
</div> {% else %}
<button class="btn btn-primary btn-sm h-8" type="submit">Добавить</button> <div class="text-sm text-app-textMuted italic py-2">Расписание пусто</div>
</form> {% endfor %}
<div class="overflow-x-auto border border-app-border rounded-xl">
<table class="w-full text-left text-xs whitespace-nowrap bg-app-surface">
<thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
<tr><th class="px-3 py-2">Время Екб</th><th class="px-3 py-2">Постов</th><th class="px-3 py-2">Статус</th><th class="px-3 py-2"></th></tr>
</thead>
<tbody class="divide-y divide-app-border">
{% for row in vk_schedule %}
<tr class="hover:bg-app-surfaceHover transition-colors">
<td class="px-3 py-2">
<form id="vk-schedule-update-{{ row.id }}" method="post" action="/vk-poster-schedule/{{ row.id }}/update" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</form>
<input form="vk-schedule-update-{{ row.id }}" type="time" name="time" value="{{ row.time }}" class="input input-sm w-full bg-app-bg h-7 text-white" required>
</td>
<td class="px-3 py-2"><input form="vk-schedule-update-{{ row.id }}" type="number" name="count" value="{{ row.count }}" min="1" max="20" class="input input-sm w-16 bg-app-bg h-7 text-white" required></td>
<td class="px-3 py-2"><label class="cursor-pointer flex justify-center"><input form="vk-schedule-update-{{ row.id }}" type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" {% if row.enabled %}checked{% endif %}></label></td>
<td class="px-3 py-2 flex gap-1 justify-center">
<button form="vk-schedule-update-{{ row.id }}" type="submit" class="btn btn-ghost btn-icon w-7 h-7 hover:text-app-success"><i data-lucide="check" class="w-4 h-4"></i></button>
<form method="post" action="/vk-poster-schedule/{{ row.id }}/delete" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="btn btn-ghost btn-icon w-7 h-7 text-app-textMuted hover:text-app-error hover:bg-app-errorBg"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<form method="post" action="/vk-poster-schedule/create" class="flex flex-wrap sm:flex-nowrap items-end gap-3 bg-app-surface p-4 rounded-xl border border-app-border">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="enabled" value="on">
<div class="w-full sm:w-32">
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время (час)</label>
<select name="time" class="select select-sm w-full font-mono">
{% for h in range(24) %}
{% set hh = "%02d" % h %}
<option value="{{ hh }}:00">{{ hh }}:00</option>
{% endfor %}
</select>
</div>
<div class="w-full sm:w-24">
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Кол-во</label>
<input type="number" name="count" value="1" min="1" max="20" class="input input-sm w-full text-center" required>
</div>
<button class="btn btn-primary btn-sm h-8 w-full sm:w-auto" type="submit">
<i data-lucide="plus" class="w-4 h-4"></i> Добавить
</button>
</form>
</div> </div>
</details> </details>
</div> </div>