fix: address user feedback on sources, raw_posts and workers UI/UX
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 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 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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user