Files
raa-parser_poster/src/vk_parser_app/templates/workers.html
T
2026-08-03 17:36:31 +05:00

419 lines
24 KiB
HTML

{% extends "base.html" %}
{% block body %}
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3 mb-2">
<i data-lucide="cpu" class="text-app-primary w-8 h-8"></i>
Система и Воркеры
</h1>
<div class="text-app-textMuted text-sm">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div>
</div>
<!-- Workers Panel -->
<div class="card overflow-hidden mb-8">
<div class="overflow-x-auto">
<table class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-app-bg border-b border-app-border">
<tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th class="px-4 py-3">Воркер</th>
<th class="px-4 py-3">Состояние</th>
<th class="px-4 py-3">Heartbeat</th>
<th class="px-4 py-3">Статус</th>
<th class="px-4 py-3">Текущая задача</th>
<th class="px-4 py-3">Действия</th>
</tr>
</thead>
<tbody class="divide-y divide-app-border text-xs">
{% for w in workers %}
<tr class="hover:bg-app-surfaceHover transition-colors">
<td class="px-4 py-3 font-mono font-bold text-white">{{ w.name }}</td>
<td class="px-4 py-3">
{% if w.effective_enabled %}
<span class="badge badge-success px-2 py-0.5 flex items-center gap-1.5 w-max">
<span class="w-1.5 h-1.5 rounded-full bg-app-success"></span> Включен
</span>
{% elif w.enabled and w.setting_enabled == false %}
<span class="badge badge-warning px-2 py-0.5 flex items-center gap-1.5 w-max">
<span class="w-1.5 h-1.5 rounded-full bg-app-warning"></span> Выключен настройкой
</span>
{% else %}
<span class="badge badge-error px-2 py-0.5 flex items-center gap-1.5 w-max">
<span class="w-1.5 h-1.5 rounded-full bg-app-error"></span> Выключен
</span>
{% endif %}
</td>
<td class="px-4 py-3 text-app-textMuted">{{ w.heartbeat_at or "—" }}</td>
<td class="px-4 py-3 max-w-xs text-app-textMain">
<div class="truncate">{{ w.status or "—" }}</div>
{% if w.meta_json and w.meta_json.error %}
<div class="mt-1 text-[10px] text-app-error whitespace-normal break-words">{{ w.meta_json.error }}</div>
{% endif %}
</td>
<td class="px-4 py-3 font-mono text-app-textMuted">{{ w.current_job_id or "—" }}</td>
<td class="px-4 py-3">
<form method="post" action="/workers/{{ w.name }}/toggle" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-xs btn-surface" type="submit">Переключить</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-8">
<!-- Categories -->
<details class="card group/details" open>
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
<div class="flex items-center gap-2 font-bold text-lg text-white">
<i data-lucide="tags" class="w-5 h-5 text-app-primary"></i> Категории публикаций
</div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary>
<div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<div class="text-sm text-app-textMuted">
Название описывает категорию для AI, тэг используется в соцсетях.
</div>
<form method="post" action="/categories/create" class="flex flex-col gap-4 bg-app-surface p-4 rounded-xl border border-app-border">
<div class="font-bold text-sm text-white">Добавить категорию</div>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Название (для AI)</label>
<input name="name" class="input w-full" required>
</div>
<div>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Тэг (для хэштегов)</label>
<input name="tag" class="input w-full">
</div>
<div>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">На сайте</label>
<input name="site_name" class="input w-full" required>
</div>
<div>
<label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">URL-slug</label>
<input name="site_slug" class="input w-full" pattern="[a-z0-9-]+" required>
</div>
</div>
<div class="flex items-center justify-between mt-2">
<label class="cursor-pointer flex items-center gap-2 group">
<input type="checkbox" name="site_enabled" class="checkbox" checked>
<span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Публиковать</span>
</label>
<button class="btn btn-primary btn-sm" type="submit">Добавить</button>
</div>
</form>
<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">Сайт / Slug</th>
<th class="px-3 py-2 text-center">ID</th>
<th class="px-3 py-2">Статус</th>
<th class="px-3 py-2 w-16"></th>
</tr>
</thead>
<tbody class="divide-y divide-app-border">
{% for c in categories %}
<tr class="hover:bg-app-surfaceHover transition-colors">
<td class="px-3 py-2">
<form id="category-update-{{ c.id }}" method="post" action="/categories/{{ c.id }}/update" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</form>
<div class="flex flex-col gap-1.5">
<input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-sm w-full bg-app-bg text-white h-7">
<input form="category-update-{{ c.id }}" name="tag" value="{{ c.tag }}" class="input input-sm w-full bg-app-bg text-blue-400 font-mono h-7">
</div>
</td>
<td class="px-3 py-2">
<div class="flex flex-col gap-1.5">
<input form="category-update-{{ c.id }}" name="site_name" value="{{ c.site_name }}" class="input input-sm w-full bg-app-bg text-white h-7" required>
<input form="category-update-{{ c.id }}" name="site_slug" value="{{ c.site_slug }}" pattern="[a-z0-9-]+" class="input input-sm w-full bg-app-bg text-app-textMuted font-mono h-7" required>
</div>
</td>
<td class="px-3 py-2 text-center font-mono text-app-textMuted text-[10px]">{{ c.sort_order }}</td>
<td class="px-3 py-2">
<div class="flex flex-col gap-2">
<span class="badge {% if c.is_active %}badge-success{% else %}badge-neutral{% endif %} w-full py-0.5 text-[10px]">{% if c.is_active %}Активна{% else %}Выкл{% endif %}</span>
<label class="cursor-pointer flex items-center gap-2 bg-app-bg px-2 py-1 rounded border border-app-border">
<input form="category-update-{{ c.id }}" type="checkbox" name="site_enabled" class="checkbox w-3.5 h-3.5 rounded-sm" {% if c.site_enabled %}checked{% endif %}>
<span class="text-[10px] font-medium text-app-textMuted">Сайт</span>
</label>
</div>
</td>
<td class="px-3 py-2">
<div class="flex flex-col gap-1 items-center">
<button form="category-update-{{ c.id }}" type="submit" class="btn btn-ghost btn-icon w-7 h-7 hover:text-app-success" title="Сохранить"><i data-lucide="check" class="w-4 h-4"></i></button>
<form method="post" action="/categories/{{ c.id }}/toggle" class="m-0 w-full flex justify-center">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="btn btn-ghost btn-icon w-7 h-7" title="Вкл/Выкл"><i data-lucide="{% if c.is_active %}pause{% else %}play{% endif %}" class="w-4 h-4"></i></button>
</form>
<form method="post" action="/categories/{{ c.id }}/delete" class="m-0 w-full flex justify-center" onsubmit="return confirm('Удалить категорию?');">
<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" title="Удалить"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</details>
<div class="flex flex-col gap-8">
<!-- TG Schedule -->
<details class="card group/details" open>
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
<div class="flex items-center gap-2 font-bold text-lg text-white">
<i data-lucide="send" class="w-5 h-5 text-[#0088cc]"></i> Расписание TG-постера
</div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary>
<div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<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>.
</div>
<!-- Active Schedule Chips -->
<div class="flex flex-wrap gap-2">
{% for row in tg_schedule %}
{% if row.enabled %}
<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">
<span class="font-mono text-sm font-bold text-white">{{ row.time[:2] }}:00</span>
<span class="text-xs text-app-textMuted border-l border-app-border pl-2">{{ row.count }} шт.</span>
<form method="post" action="/tg-poster-schedule/{{ row.id }}/delete" class="m-0 ml-1 flex items-center">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="text-app-textMuted hover:text-app-error transition-colors" title="Удалить">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</form>
</div>
{% endif %}
{% else %}
<div class="text-sm text-app-textMuted italic py-2">Расписание пусто</div>
{% endfor %}
</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>
</details>
<!-- VK Schedule -->
<details class="card group/details" open>
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
<div class="flex items-center gap-2 font-bold text-lg text-white">
<i data-lucide="layout-template" class="w-5 h-5 text-[#4680C2]"></i> Расписание VK-постера
</div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary>
<div class="p-4 flex flex-col gap-6 bg-app-bg/30">
<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>.
</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>
<!-- Active Schedule Chips -->
<div class="flex flex-wrap gap-2">
{% for row in vk_schedule %}
{% if row.enabled %}
<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">
<span class="font-mono text-sm font-bold text-white">{{ row.time[:2] }}:00</span>
<span class="text-xs text-app-textMuted border-l border-app-border pl-2">{{ row.count }} шт.</span>
<form method="post" action="/vk-poster-schedule/{{ row.id }}/delete" class="m-0 ml-1 flex items-center">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="text-app-textMuted hover:text-app-error transition-colors" title="Удалить">
<i data-lucide="x" class="w-3.5 h-3.5"></i>
</button>
</form>
</div>
{% endif %}
{% else %}
<div class="text-sm text-app-textMuted italic py-2">Расписание пусто</div>
{% endfor %}
</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>
</details>
</div>
</div>
<div class="flex items-center gap-4 mb-6 mt-12">
<div class="h-px bg-app-border flex-1"></div>
<h2 class="text-2xl font-bold text-white flex items-center gap-3">
<i data-lucide="settings" class="w-6 h-6 text-app-primary"></i> Глобальные настройки
</h2>
<div class="h-px bg-app-border flex-1"></div>
</div>
<div class="flex flex-col gap-6">
{% for category, rows in settings|groupby("category") %}
<details class="card group/details" {% if category == "AI Qualifier" or loop.first %}open{% endif %}>
<summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors border-b border-app-border list-none">
<div class="text-lg font-bold text-white">{{ category_titles.get(category, category) }}</div>
<i data-lucide="chevron-down" class="w-5 h-5 text-app-textMuted transition-transform group-open/details:rotate-180"></i>
</summary>
<div class="p-6 bg-app-bg/30">
<div class="flex flex-col gap-8">
{% for s in rows %}
<div class="flex flex-col md:flex-row gap-6 p-5 bg-app-surface border border-app-border rounded-xl shadow-sm">
<!-- Description Sidebar -->
<div class="w-full md:w-1/3 flex flex-col gap-1 border-b md:border-b-0 md:border-r border-app-border pb-4 md:pb-0 pr-0 md:pr-4">
<div class="font-mono font-bold text-sm text-app-primary">{{ s.key }}</div>
<div class="text-sm font-semibold text-white mt-1">{{ s.title }}</div>
<div class="text-xs text-app-textMuted mt-2 leading-relaxed">{{ s.description }}</div>
</div>
<!-- Control Panel -->
<div class="flex-1">
<form method="post" action="/settings/save" class="flex flex-col items-start gap-3 w-full h-full justify-center">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="key" value="{{ s.key }}">
<div class="flex gap-3 w-full items-start">
<div class="flex-1">
{% if s.value_type == "bool" %}
<select name="value" class="select w-32">
<option value="true" {% if s.value_json == true %}selected{% endif %}>true</option>
<option value="false" {% if s.value_json == false %}selected{% endif %}>false</option>
</select>
{% elif s.key in ["ai_qualifier_provider", "ai_writer_provider"] %}
<select name="value" class="select max-w-sm w-full">
{% for p in provider_options %}
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
{% endfor %}
</select>
{% elif s.key == "site_poster_provider" %}
<select name="value" class="select max-w-sm w-full">
{% for p in site_poster_provider_options %}
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
{% endfor %}
</select>
{% elif s.key == "ai_qualifier_model" %}
<select name="value" class="select max-w-sm w-full">
{% for m in ai_model_options %}
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
{% endfor %}
{% if s.value_json and s.value_json not in ai_model_options|map(attribute="value")|list %}
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
{% endif %}
</select>
{% elif s.key == "ai_writer_model" %}
<select name="value" class="select max-w-sm w-full">
{% for m in writer_model_options %}
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
{% endfor %}
{% if s.value_json and s.value_json not in writer_model_options|map(attribute="value")|list %}
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
{% endif %}
</select>
{% elif s.value_type == "secret" %}
<input type="password" name="value" value="{{ s.value_json }}" class="input w-full max-w-md" autocomplete="off" spellcheck="false" placeholder="Секретный ключ">
{% elif s.value_type == "text" %}
<div class="w-full flex flex-col gap-3">
{% if prompt_hints and s.key in prompt_hints %}
<div class="bg-app-primary/10 border border-app-primary/30 text-app-textMain text-xs p-4 rounded-lg flex flex-col gap-2">
<strong class="text-app-primary font-bold flex items-center gap-2"><i data-lucide="info" class="w-4 h-4"></i> {{ prompt_hints[s.key].title }}</strong>
<div class="leading-relaxed">{{ prompt_hints[s.key].body }}</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-2 pt-2 border-t border-app-primary/20">
<div><span class="font-bold text-app-textMain uppercase tracking-wider text-[10px]">Как безопасно менять:</span><br>{{ prompt_hints[s.key].safe }}</div>
<div><span class="font-bold text-app-textMain uppercase tracking-wider text-[10px]">На вход:</span><br>{{ prompt_hints[s.key].input }}</div>
</div>
</div>
{% endif %}
<textarea name="value" class="textarea font-mono text-xs w-full leading-relaxed p-4 bg-app-bg" rows="12">{{ s.value_json }}</textarea>
</div>
{% elif s.value_type == "json" %}
<textarea name="value" class="textarea font-mono text-xs w-full max-w-2xl leading-relaxed p-4 bg-app-bg" rows="5" spellcheck="false">{{ s.value_json | tojson }}</textarea>
{% else %}
<input name="value" value="{{ s.value_json }}" class="input w-full max-w-md">
{% endif %}
</div>
<button class="btn btn-primary" type="submit">
<i data-lucide="save" class="w-4 h-4"></i> Сохранить
</button>
</div>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
</details>
{% endfor %}
</div>
<script>
(() => {
const scrollKey = "workers-scroll-y";
const restoreY = sessionStorage.getItem(scrollKey);
if (restoreY !== null) {
sessionStorage.removeItem(scrollKey);
requestAnimationFrame(() => window.scrollTo(0, Number(restoreY) || 0));
}
document.addEventListener("submit", (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const action = form.getAttribute("action") || "";
if (action.startsWith("/workers") || action.startsWith("/settings") || action.includes("-schedule/")) {
sessionStorage.setItem(scrollKey, String(window.scrollY));
}
}, true);
})();
</script>
{% endblock %}