feat: complete UI redesign of admin dashboard with custom Tailwind

This commit is contained in:
Your Name
2026-07-18 22:59:37 +05:00
parent 9e63ebadff
commit 8bca008f74
12 changed files with 1307 additions and 838 deletions
+232 -35
View File
@@ -1,62 +1,259 @@
<!doctype html> <!doctype html>
<html lang="ru" data-theme="light"> <html lang="ru" class="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favi.png"> <link rel="icon" type="image/png" href="/favi.png">
<title>{{ title or "VK Parser Admin" }}</title> <title>{{ title or "VK Parser Admin" }}</title>
<!-- Tailwind CSS & DaisyUI --> <!-- Fonts: Fira Sans (UI) & Fira Code (Data) -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.10.1/dist/full.min.css" rel="stylesheet" type="text/css" /> <link rel="preconnect" href="https://fonts.googleapis.com">
<script src="https://cdn.tailwindcss.com"></script> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- HTMX --> <!-- Tailwind CSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['"Fira Sans"', 'sans-serif'],
mono: ['"Fira Code"', 'monospace'],
},
colors: {
app: {
bg: '#0F172A', // slate-900 (Main Background)
surface: '#1E293B', // slate-800 (Cards, Sidebar)
surfaceHover: '#334155', // slate-700
border: '#334155', // slate-700
borderFocus: '#475569',// slate-600
primary: '#3B82F6', // blue-500
primaryHover: '#2563EB',// blue-600
textMain: '#F8FAFC', // slate-50
textMuted: '#94A3B8', // slate-400
success: '#10B981', // emerald-500
successBg: 'rgba(16, 185, 129, 0.1)',
warning: '#F59E0B', // amber-500
warningBg: 'rgba(245, 158, 11, 0.1)',
error: '#EF4444', // red-500
errorBg: 'rgba(239, 68, 68, 0.1)',
}
},
boxShadow: {
'glow': '0 0 15px rgba(59, 130, 246, 0.3)',
}
}
}
}
</script>
<!-- HTMX & AlpineJS -->
<script src="https://unpkg.com/htmx.org@1.9.11"></script> <script src="https://unpkg.com/htmx.org@1.9.11"></script>
<!-- AlpineJS (optional but useful for local state) -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.8/dist/cdn.min.js"></script>
<style> <!-- Lucide Icons -->
/* Custom tweaks to match Tailwind typography nicely */ <script src="https://unpkg.com/lucide@latest"></script>
body { font-family: 'Inter', system-ui, sans-serif; }
.htmx-indicator { display:none; } <style type="text/tailwindcss">
.htmx-request .htmx-indicator { display:inline-block; } @layer components {
.htmx-request.htmx-indicator { display:inline-block; } /* Form inputs */
.input {
@apply bg-app-bg border border-app-border rounded-lg px-3 py-2 text-sm text-app-textMain focus:outline-none focus:border-app-primary focus:ring-1 focus:ring-app-primary transition-colors w-full placeholder:text-app-textMuted;
}
.select {
@apply input appearance-none bg-no-repeat bg-[right_0.5rem_center] bg-[length:1.5em_1.5em];
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2394A3B8' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
}
.textarea {
@apply bg-app-bg border border-app-border rounded-lg px-3 py-2 text-sm text-app-textMain focus:outline-none focus:border-app-primary focus:ring-1 focus:ring-app-primary transition-colors w-full placeholder:text-app-textMuted;
}
.checkbox {
@apply w-4 h-4 rounded border-app-border bg-app-bg text-app-primary focus:ring-app-primary focus:ring-offset-app-surface;
}
/* Buttons */
.btn {
@apply inline-flex items-center justify-center gap-2 rounded-lg font-medium text-sm px-4 py-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-app-bg;
}
.btn-primary {
@apply bg-app-primary text-white hover:bg-app-primaryHover focus:ring-app-primary border border-transparent;
}
.btn-surface {
@apply bg-app-surface border border-app-border text-app-textMain hover:bg-app-surfaceHover focus:ring-app-border;
}
.btn-ghost {
@apply text-app-textMuted hover:text-app-textMain hover:bg-app-surfaceHover border border-transparent;
}
.btn-danger {
@apply bg-app-error text-white hover:bg-red-600 focus:ring-app-error border border-transparent;
}
.btn-danger-outline {
@apply bg-transparent border border-app-error text-app-error hover:bg-app-error hover:text-white focus:ring-app-error;
}
.btn-primary-outline {
@apply bg-transparent border border-app-primary text-app-primary hover:bg-app-primary hover:text-white focus:ring-app-primary;
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
.btn-xs {
@apply px-2 py-1 text-xs;
}
.btn-icon {
@apply p-2;
}
/* Cards */
.card {
@apply bg-app-surface border border-app-border rounded-xl shadow-sm overflow-hidden;
}
/* Badges */
.badge {
@apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border;
}
.badge-success {
@apply bg-app-successBg text-app-success border-app-success/20;
}
.badge-error {
@apply bg-app-errorBg text-app-error border-app-error/20;
}
.badge-warning {
@apply bg-app-warningBg text-app-warning border-app-warning/20;
}
.badge-neutral {
@apply bg-app-bg text-app-textMuted border-app-border;
}
.badge-primary {
@apply bg-app-primary/10 text-app-primary border-app-primary/20;
}
.badge-info {
@apply bg-blue-500/10 text-blue-400 border-blue-500/20;
}
/* Tabs */
.tabs {
@apply flex space-x-1 bg-app-bg p-1 rounded-lg border border-app-border overflow-x-auto;
}
.tab {
@apply flex items-center px-3 py-1.5 text-sm font-medium rounded-md text-app-textMuted hover:text-app-textMain transition-colors whitespace-nowrap;
}
.tab-active {
@apply bg-app-surface text-app-textMain shadow-sm border border-app-border;
}
/* HTMX Indicators */
.htmx-indicator { display:none; }
.htmx-request .htmx-indicator { display:inline-block; }
.htmx-request.htmx-indicator { display:inline-block; }
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { @apply bg-transparent; }
::-webkit-scrollbar-thumb { @apply bg-app-border rounded-full hover:bg-app-borderFocus; }
}
</style> </style>
</head> </head>
<body class="bg-base-200 min-h-screen"> <body class="bg-app-bg text-app-textMain min-h-screen font-sans flex antialiased">
{% if user %} {% if user %}
<div class="navbar bg-base-100 shadow-sm sticky top-0 z-50"> <!-- Sidebar -->
<div class="flex-1 px-2 mx-2"> <aside class="w-64 flex-shrink-0 bg-app-surface border-r border-app-border flex flex-col hidden md:flex sticky top-0 h-screen">
<span class="text-lg font-bold">Parser Admin</span> <div class="p-6">
<div class="flex items-center gap-3 text-app-textMain font-bold text-lg tracking-wide">
<i data-lucide="zap" class="w-6 h-6 text-app-primary"></i>
<span>Parser Admin</span>
</div>
</div> </div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1"> <nav class="flex-1 px-4 space-y-1 overflow-y-auto">
<li><a href="/raw" class="{{ 'active' if 'raw' in request.url.path }}">Сырые посты</a></li> <a href="/raw" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'raw' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<li><a href="/editor" class="{{ 'active' if 'editor' in request.url.path }}">Редактор</a></li> <i data-lucide="inbox" class="w-5 h-5"></i>
<li><a href="/sources" class="{{ 'active' if 'sources' in request.url.path }}">Источники</a></li> Сырые посты
<li><a href="/workers" class="{{ 'active' if 'workers' in request.url.path }}">Воркеры</a></li> </a>
</ul> <a href="/editor" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'editor' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<i data-lucide="edit-3" class="w-5 h-5"></i>
Редактор
</a>
<a href="/sources" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'sources' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<i data-lucide="database" class="w-5 h-5"></i>
Источники
</a>
<a href="/workers" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'workers' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<i data-lucide="cpu" class="w-5 h-5"></i>
Воркеры
</a>
<a href="/users" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'users' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<i data-lucide="users" class="w-5 h-5"></i>
Пользователи
</a>
<a href="/logs" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ 'bg-app-primary/10 text-app-primary' if 'logs' in request.url.path else 'text-app-textMuted hover:bg-app-surfaceHover hover:text-app-textMain' }}">
<i data-lucide="file-text" class="w-5 h-5"></i>
Логи
</a>
</nav>
<div class="p-4 border-t border-app-border">
<div class="flex items-center gap-3 px-3 py-2 mb-2">
<div class="w-8 h-8 rounded-full bg-app-primary/20 flex items-center justify-center text-app-primary font-bold text-sm">
{{ user.login[0]|upper }}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-app-textMain truncate">{{ user.login }}</div>
<div class="text-xs text-app-textMuted truncate">{{ user.role }}</div>
</div>
</div>
<form method="post" action="/logout" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-app-error hover:bg-app-errorBg transition-colors">
<i data-lucide="log-out" class="w-4 h-4"></i>
Выйти
</button>
</form>
</div> </div>
<div class="flex-none hidden lg:block"> </aside>
<ul class="menu menu-horizontal gap-2">
<li> <!-- Mobile Topbar (Visible only on small screens) -->
<form method="post" action="/logout" class="m-0"> <div class="md:hidden fixed top-0 left-0 right-0 h-16 bg-app-surface border-b border-app-border z-50 flex items-center justify-between px-4">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <div class="flex items-center gap-2 font-bold text-app-textMain">
<button class="btn btn-ghost btn-sm text-error">Выйти</button> <i data-lucide="zap" class="w-5 h-5 text-app-primary"></i>
</form> Parser
</li> </div>
</ul> <button class="btn btn-ghost btn-icon" onclick="document.getElementById('mobile-menu').classList.toggle('hidden')">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
<!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden fixed top-16 left-0 right-0 bg-app-surface border-b border-app-border z-40 p-4 space-y-2 shadow-lg">
<a href="/raw" class="block px-3 py-2 rounded-lg text-sm {{ 'bg-app-bg text-app-textMain' if 'raw' in request.url.path else 'text-app-textMuted' }}">Сырые посты</a>
<a href="/editor" class="block px-3 py-2 rounded-lg text-sm {{ 'bg-app-bg text-app-textMain' if 'editor' in request.url.path else 'text-app-textMuted' }}">Редактор</a>
<a href="/sources" class="block px-3 py-2 rounded-lg text-sm {{ 'bg-app-bg text-app-textMain' if 'sources' in request.url.path else 'text-app-textMuted' }}">Источники</a>
<a href="/workers" class="block px-3 py-2 rounded-lg text-sm {{ 'bg-app-bg text-app-textMain' if 'workers' in request.url.path else 'text-app-textMuted' }}">Воркеры</a>
<div class="pt-2 mt-2 border-t border-app-border">
<form method="post" action="/logout" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="w-full text-left px-3 py-2 rounded-lg text-sm text-app-error">Выйти</button>
</form>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<main class="container mx-auto p-4 md:p-6 max-w-7xl"> <!-- Main Content Area -->
{% block body %}{% endblock %} <main class="flex-1 min-w-0 flex flex-col h-screen overflow-y-auto {% if user %}pt-16 md:pt-0{% endif %} relative">
<div class="p-4 md:p-8 max-w-[1600px] w-full mx-auto">
{% block body %}{% endblock %}
</div>
</main> </main>
<script> <script>
// Handle HTMX errors // Initialize Lucide icons
lucide.createIcons();
// Re-initialize icons after HTMX swap
document.body.addEventListener('htmx:afterSwap', function() {
lucide.createIcons();
});
document.body.addEventListener('htmx:responseError', function(evt) { document.body.addEventListener('htmx:responseError', function(evt) {
alert("Ошибка при загрузке: " + evt.detail.xhr.status); alert("Ошибка при загрузке: " + evt.detail.xhr.status);
}); });
+11 -4
View File
@@ -2,14 +2,21 @@
{% block body %} {% block body %}
<div class="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> <div class="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div> <div>
<h1 class="text-3xl font-bold">Редакторская</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
<div class="text-base-content/60 mt-1">Посты после AI-рерайта: быстрая проверка, правка и принятие.</div> <i data-lucide="edit-3" class="text-app-primary w-8 h-8"></i>
Редакторская
</h1>
<div class="text-app-textMuted mt-1 text-sm">Проверка, правка и публикация сгенерированных постов.</div>
</div> </div>
<div class="tabs tabs-boxed bg-base-200">
<!-- Status Tabs -->
<div class="tabs p-1">
{% for st in status_options %} {% for st in status_options %}
<a class="tab {% if st.value in editorial_statuses %}tab-active{% endif %}" href="/editor?editorial_status={{ st.value }}"> <a class="tab {% if st.value in editorial_statuses %}tab-active{% endif %}" href="/editor?editorial_status={{ st.value }}">
{{ st.label }} {{ st.label }}
<span class="badge badge-sm ml-2 {% if st.value in editorial_statuses %}badge-primary{% else %}badge-ghost{% endif %}">{{ counts.get(st.value, 0) }}</span> <span class="ml-2 px-1.5 py-0.5 rounded-full text-xs font-semibold {% if st.value in editorial_statuses %}bg-app-primary/20 text-app-primary{% else %}bg-app-surface text-app-textMuted{% endif %}">
{{ counts.get(st.value, 0) }}
</span>
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
+69 -68
View File
@@ -1,24 +1,25 @@
<div class="flex flex-col lg:flex-row gap-6"> <div class="flex flex-col xl:flex-row gap-6">
<!-- Sidebar Filters --> <!-- Sidebar Filters -->
<aside class="w-full lg:w-72 flex-shrink-0"> <aside class="w-full xl:w-72 flex-shrink-0">
<div class="card bg-base-100 shadow-sm border border-base-200"> <div class="card p-4 sticky top-6">
<div class="card-body p-4"> <form id="filter-form" hx-get="/editor" hx-target="#content-area" hx-push-url="true" hx-trigger="submit, change delay:300ms">
<form hx-get="/editor" hx-target="#content-area" hx-push-url="true" hx-trigger="submit, change delay:300ms"> {% for item in preserved_query %}
{% for item in preserved_query %} {% if item.key != "per_page" and item.key != "sort" and item.key != "editorial_status" %}
{% if item.key != "per_page" and item.key != "sort" and item.key != "editorial_status" %} <input type="hidden" name="{{ item.key }}" value="{{ item.value }}">
<input type="hidden" name="{{ item.key }}" value="{{ item.value }}"> {% endif %}
{% endif %} {% endfor %}
{% endfor %} <input type="hidden" name="editorial_status" value="{{ status_filter }}">
<input type="hidden" name="editorial_status" value="{{ status_filter }}">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-6">
<h2 class="card-title text-lg">Фильтры</h2> <h2 class="text-sm font-bold text-app-textMuted uppercase tracking-wider">Фильтры</h2>
<a href="/editor" class="text-sm link link-hover text-base-content/60" hx-boost="true" hx-target="#content-area">Сбросить</a> <a href="/editor" class="text-xs text-app-primary hover:text-app-primaryHover transition-colors" hx-boost="true" hx-target="#content-area">Сбросить</a>
</div> </div>
<div class="form-control w-full mb-4"> <div class="space-y-6">
<label class="label"><span class="label-text font-bold">Сортировка</span></label> <!-- Sort -->
<select name="sort" class="select select-bordered select-sm w-full"> <div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Сортировка</label>
<select name="sort" class="select w-full">
<option value="rewritten_desc" {% if sort == "rewritten_desc" %}selected{% endif %}>рерайт: новые</option> <option value="rewritten_desc" {% if sort == "rewritten_desc" %}selected{% endif %}>рерайт: новые</option>
<option value="rewritten_asc" {% if sort == "rewritten_asc" %}selected{% endif %}>рерайт: старые</option> <option value="rewritten_asc" {% if sort == "rewritten_asc" %}selected{% endif %}>рерайт: старые</option>
<option value="posted_desc" {% if sort == "posted_desc" %}selected{% endif %}>VK: новые</option> <option value="posted_desc" {% if sort == "posted_desc" %}selected{% endif %}>VK: новые</option>
@@ -28,63 +29,68 @@
</select> </select>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Score -->
<label class="label"><span class="label-text font-bold">Оценка AI</span></label> <div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Оценка AI</label>
<div class="flex gap-2"> <div class="flex gap-2">
<input type="number" name="score_min" placeholder="От" value="{{ score_min }}" class="input input-bordered input-sm w-full" /> <input type="number" name="score_min" placeholder="От" value="{{ score_min }}" class="input w-full" />
<input type="number" name="score_max" placeholder="До" value="{{ score_max }}" class="input input-bordered input-sm w-full" /> <input type="number" name="score_max" placeholder="До" value="{{ score_max }}" class="input w-full" />
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Dates -->
<label class="label"><span class="label-text font-bold">Дата поста VK</span></label> <div>
<div class="flex flex-col gap-2"> <label class="block text-xs font-semibold text-app-textMain mb-2">Дата поста VK</label>
<input type="date" name="date_from" value="{{ date_from }}" class="input input-bordered input-sm w-full" /> <div class="space-y-2">
<input type="date" name="date_to" value="{{ date_to }}" class="input input-bordered input-sm w-full" /> <input type="date" name="date_from" value="{{ date_from }}" class="input w-full" />
<input type="date" name="date_to" value="{{ date_to }}" class="input w-full" />
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Source -->
<label class="label"><span class="label-text font-bold">Источник</span></label> <div>
<div class="bg-base-200 rounded-lg p-2 max-h-48 overflow-y-auto"> <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">
{% for item in facets.sources %} {% for item in facets.sources %}
<label class="label cursor-pointer justify-start gap-2 p-1"> <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.value }}" class="checkbox checkbox-sm checkbox-primary" {% if item.value|string in source_ids %}checked{% endif %} /> <input type="checkbox" name="source_id" value="{{ item.value }}" class="checkbox" {% if item.value|string in source_ids %}checked{% endif %} />
<span class="label-text flex-1 truncate" title="{{ item.label }}">{{ item.label }}</span> <span class="text-sm text-app-textMain flex-1 truncate group-hover:text-white" title="{{ item.label }}">{{ item.label }}</span>
<span class="badge badge-sm badge-ghost">{{ item.count }}</span> <span class="badge badge-neutral bg-transparent border-none">{{ item.count }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Category -->
<label class="label"><span class="label-text font-bold">Категория</span></label> <div>
<div class="bg-base-200 rounded-lg p-2 max-h-48 overflow-y-auto"> <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">
{% for item in facets.categories %} {% for item in facets.categories %}
<label class="label cursor-pointer justify-start gap-2 p-1"> <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="category" value="{{ item.value }}" class="checkbox checkbox-sm checkbox-primary" {% if item.value in categories_selected %}checked{% endif %} /> <input type="checkbox" name="category" value="{{ item.value }}" class="checkbox" {% if item.value in categories_selected %}checked{% endif %} />
<span class="label-text flex-1">{{ item.value or "Без категории" }}</span> <span class="text-sm text-app-textMain flex-1 group-hover:text-white">{{ item.value or "Без категории" }}</span>
<span class="badge badge-sm badge-ghost">{{ item.count }}</span> <span class="badge badge-neutral bg-transparent border-none">{{ item.count }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<button class="btn btn-primary w-full mt-4" type="submit">Применить</button> <button class="btn btn-primary w-full" type="submit">Применить фильтры</button>
</form> </div>
</div> </form>
</div> </div>
</aside> </aside>
<!-- Main Content (List) --> <!-- Main Content (List) -->
<div class="flex-1 min-w-0 flex flex-col gap-4"> <div class="flex-1 min-w-0 flex flex-col gap-6">
<div class="flex justify-between items-center px-2"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-app-surface border border-app-border p-3 rounded-xl shadow-sm">
<div class="text-sm text-base-content/70"> <div class="text-sm text-app-textMuted flex items-center gap-2">
Всего постов: <span class="font-bold">{{ pagination.total }}</span> <i data-lucide="layers" class="w-4 h-4"></i>
Найдено: <span class="font-bold text-white">{{ pagination.total }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm">На странице:</span> <span class="text-sm text-app-textMuted">На странице:</span>
<select name="per_page" class="select select-bordered select-sm" form="pagination-form" hx-get="/editor" hx-target="#content-area"> <select name="per_page" class="select select-sm w-auto py-1" form="filter-form">
{% for n in [10,25,50,100] %} {% for n in [10,25,50,100] %}
<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 %}
@@ -96,8 +102,10 @@
{% for p in posts %} {% for p in posts %}
{% include "editor_post.html" %} {% include "editor_post.html" %}
{% else %} {% else %}
<div class="card bg-base-100 shadow-sm border border-base-200 p-8 text-center text-base-content/60"> <div class="card p-12 flex flex-col items-center justify-center text-center">
В этой очереди пока нет постов. <i data-lucide="inbox" class="w-16 h-16 text-app-borderFocus mb-4"></i>
<h3 class="text-lg font-bold text-white mb-1">Ничего не найдено</h3>
<p class="text-app-textMuted text-sm">В этой очереди пока нет постов, подходящих под фильтры.</p>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -105,24 +113,17 @@
<!-- Pagination --> <!-- Pagination -->
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="flex justify-center mt-4"> <div class="flex justify-center mt-4">
<form id="pagination-form" hx-get="/editor" hx-target="#content-area" hx-push-url="true" class="join"> <div class="tabs p-1">
<input type="hidden" name="sort" value="{{ sort }}"> <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>
<input type="hidden" name="score_min" value="{{ score_min }}"> <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>
<input type="hidden" name="score_max" value="{{ score_max }}">
<input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="editorial_status" value="{{ status_filter }}">
{% for cat in categories_selected %}<input type="hidden" name="category" value="{{ cat }}">{% endfor %}
{% for sid in source_ids %}<input type="hidden" name="source_id" value="{{ sid }}">{% endfor %}
<button class="join-item btn btn-sm" name="page" value="1" {% if pagination.page == 1 %}disabled{% endif %}>«</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">
<button class="join-item btn btn-sm" name="page" value="{{ pagination.page - 1 }}" {% if pagination.page == 1 %}disabled{% endif %}></button> {{ pagination.page }} из {{ pagination.pages }}
</span>
<button class="join-item btn btn-sm btn-disabled">Стр. {{ pagination.page }} из {{ pagination.pages }}</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 >= 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 class="join-item btn btn-sm" name="page" value="{{ pagination.page + 1 }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}></button> </div>
<button class="join-item btn btn-sm" name="page" value="{{ pagination.pages }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}>»</button>
</form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
+227 -132
View File
@@ -1,164 +1,259 @@
<div id="post-container-{{ p.id }}" class="card bg-base-100 shadow-sm border border-base-200 overflow-hidden"> <div id="post-container-{{ p.id }}" class="card flex flex-col group relative transition-colors border border-app-border {% if p.editorial_status == 'accepted' %}bg-app-successBg/10 border-app-success/20{% elif p.editorial_status == 'rejected' %}bg-app-errorBg/10 opacity-75{% else %}hover:border-app-borderFocus hover:shadow-glow{% endif %}">
<div class="card-body p-0 flex flex-col md:flex-row">
<!-- Media Column --> <div class="flex flex-col xl:flex-row">
<div class="w-full md:w-48 bg-base-200 border-r border-base-200 flex flex-col items-center justify-center p-4"> <!-- Left: Meta Info & Status -->
<div class="grid grid-cols-2 gap-2 mb-2 w-full"> <div class="w-full xl:w-56 bg-app-bg/50 border-b xl:border-b-0 xl:border-r border-app-border p-4 flex flex-col gap-3">
{% for m in p.media_items %} <div class="flex items-center justify-between">
{% if m.type == "photo" and m.url %} <span class="text-xs font-mono text-app-textMuted tracking-wider">#{{ p.id }}</span>
<div class="aspect-square bg-base-300 rounded overflow-hidden"> {% if p.editorial_status == 'accepted' or p.editorial_status == 'published' %}
<img src="{{ m.url }}" class="w-full h-full object-cover" alt="photo" title="{{ m.status }}"> <div class="flex items-center gap-1 text-xs font-medium text-app-success bg-app-success/10 px-2 py-1 rounded-md">
<i data-lucide="check-circle-2" class="w-3 h-3"></i> Принят
</div> </div>
{% elif m.url %} {% elif p.editorial_status == 'rejected' or p.editorial_status == 'publish_failed' %}
<a href="{{ m.url }}" target="_blank" class="aspect-square bg-base-300 rounded flex items-center justify-center hover:bg-base-content/20 text-xl" title="{{ m.error or m.status }}"></a> <div class="flex items-center gap-1 text-xs font-medium text-app-error bg-app-error/10 px-2 py-1 rounded-md">
{% endif %} <i data-lucide="x-circle" class="w-3 h-3"></i> Отклонен
{% endfor %} </div>
{% elif p.editorial_status == 'regenerating' %}
<div class="flex items-center gap-1 text-xs font-medium text-app-warning bg-app-warning/10 px-2 py-1 rounded-md">
<i data-lucide="refresh-cw" class="w-3 h-3 animate-spin"></i> В работе
</div>
{% else %}
<div class="flex items-center gap-1 text-xs font-medium text-blue-400 bg-blue-400/10 px-2 py-1 rounded-md">
<i data-lucide="clock" class="w-3 h-3"></i> Ожидает
</div>
{% endif %}
</div> </div>
<div class="text-xs text-base-content/60 font-bold uppercase tracking-widest">{{ p.media_count or 0 }} медиа</div>
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded bg-app-surface border border-app-border flex items-center justify-center flex-shrink-0">
<i data-lucide="link" class="w-4 h-4 text-app-textMuted"></i>
</div>
<div class="flex flex-col min-w-0">
<a href="{{ p.original_url }}" target="_blank" class="text-sm font-semibold text-app-textMain hover:text-app-primary truncate transition-colors" title="{{ p.source_name }}">{{ p.source_name }}</a>
<div class="text-xs text-app-textMuted truncate">{{ p.posted_at_fmt or p.created_at_fmt }}</div>
</div>
</div>
<div class="space-y-1 mt-1">
<div class="flex justify-between text-xs">
<span class="text-app-textMuted">AI Оценка:</span>
<span class="font-mono text-white font-medium">{{ p.qualification_score or "?" }}/10</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-app-textMuted">Категория:</span>
<span class="truncate ml-2 font-medium text-white" title="{{ p.review_category }}">{{ p.review_category or "—" }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-app-textMuted">Тег:</span>
<span class="font-mono text-blue-400 font-medium">#{{ p.review_source_tag or p.source_tag or "none" }}</span>
</div>
</div>
{% if p.publication_platforms %}
<div class="mt-2 pt-3 border-t border-app-border/50">
<div class="text-xs text-app-textMuted mb-2 font-semibold">Куда публикуем:</div>
<div class="flex flex-wrap gap-1">
{% for pub in p.publication_platforms %}
{% if pub.url %}
<a href="{{ pub.url }}" target="_blank" class="badge badge-info hover:bg-blue-500/20 transition-colors" title="{{ pub.error or pub.status_label }}">
{{ pub.label }} <i data-lucide="external-link" class="w-3 h-3 ml-1"></i>
</a>
{% else %}
<span class="badge {% if pub.status == 'error' %}badge-error{% else %}badge-neutral{% endif %}" title="{{ pub.error or pub.status_label }}">
{{ pub.label }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
</div> </div>
<!-- Main Content --> <!-- Right: Content & Actions -->
<div class="p-6 flex-1 flex flex-col gap-4"> <div class="flex-1 flex flex-col min-w-0">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-2 text-sm text-base-content/70">
<a href="{{ p.original_url }}" target="_blank" class="link link-hover font-medium text-base-content">{{ p.source_name }}</a>
<span>&bull;</span>
<span class="badge badge-sm">#{{ p.review_source_tag or p.source_tag or "source" }}</span>
<span>&bull;</span>
<span>{{ p.posted_at_fmt or p.created_at_fmt }}</span>
</div>
<div class="flex items-center gap-2">
<span class="badge {% if p.editorial_status in ['accepted', 'published'] %}badge-success{% elif p.editorial_status in ['rejected', 'publish_failed'] %}badge-error{% elif p.editorial_status == 'regenerating' %}badge-warning{% else %}badge-ghost{% endif %}">
{{ p.editorial_status_label }}
</span>
<span class="badge badge-outline">{{ p.review_category or "Без категории" }}</span>
<span class="badge badge-neutral">AI {{ p.qualification_score or "?" }}/10</span>
</div>
</div>
<div class="flex flex-wrap gap-2"> <!-- Content Preview -->
{% for pub in p.publication_platforms %} <div class="p-5 flex-1 text-sm text-app-textMain leading-relaxed whitespace-pre-wrap font-sans">
{% if pub.url %}
<a class="badge badge-sm badge-info gap-1" href="{{ pub.url }}" target="_blank" title="{{ pub.error or pub.status_label }}">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
{{ pub.label }}
</a>
{% else %}
<span class="badge badge-sm {{ 'badge-error' if pub.status == 'error' else 'badge-ghost' }}" title="{{ pub.error or pub.status_label }}">
{{ pub.label }}
</span>
{% endif %}
{% endfor %}
</div>
<div class="prose prose-sm max-w-none text-base-content whitespace-pre-wrap leading-snug">
{{ p.publication_preview_text }} {{ p.publication_preview_text }}
</div> </div>
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-lg"> <!-- Media Thumbnails -->
<input type="checkbox" /> {% if p.media_items %}
<div class="collapse-title text-sm font-medium">Исходник и AI-заметки</div> <div class="px-5 pb-4">
<div class="collapse-content text-sm flex flex-col md:flex-row gap-6 border-t border-base-300 pt-4"> <div class="flex flex-wrap gap-2">
<div class="flex-1"> {% for m in p.media_items %}
<div class="font-bold text-xs uppercase tracking-widest text-base-content/60 mb-2">Оригинал</div> {% if m.type == "photo" and m.url %}
<div class="whitespace-pre-wrap text-base-content/80">{{ p.raw_text }}</div> <a href="{{ m.url }}" target="_blank" class="block w-16 h-16 rounded-lg overflow-hidden border border-app-border hover:border-app-primary transition-colors">
</div> <img src="{{ m.url }}" class="w-full h-full object-cover" loading="lazy">
<div class="flex-1"> </a>
<div class="font-bold text-xs uppercase tracking-widest text-base-content/60 mb-2">Заметки</div> {% elif m.url %}
<div class="text-base-content/80">{{ p.rewrite_notes or p.qualification_reason or "—" }}</div> <a href="{{ m.url }}" target="_blank" class="w-16 h-16 rounded-lg bg-app-bg border border-app-border flex items-center justify-center hover:text-app-primary transition-colors text-app-textMuted">
</div> <i data-lucide="play-circle" class="w-6 h-6"></i>
</a>
{% endif %}
{% endfor %}
</div> </div>
</div> </div>
{% endif %}
<div class="flex flex-wrap gap-2 mt-2 pt-4 border-t border-base-200"> <!-- Original / Notes Details (Details block) -->
<form hx-post="/editor/{{ p.id }}/accept" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML"> <div class="px-5 pb-5">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <details class="group/details">
<button class="btn btn-primary btn-sm" type="submit">Принять</button> <summary class="flex items-center gap-2 text-xs font-semibold tracking-wide text-app-textMuted hover:text-white cursor-pointer select-none">
</form> <i data-lucide="chevron-right" class="w-4 h-4 transition-transform group-open/details:rotate-90"></i>
<button class="btn btn-outline btn-sm" onclick="editModal{{ p.id }}.showModal()">Редактировать</button> ПОСМОТРЕТЬ ИСХОДНИК И ЗАМЕТКИ
<form hx-post="/editor/{{ p.id }}/reject" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML" hx-confirm="Отклонить пост #{{ p.id }}?"> </summary>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <div class="mt-3 grid grid-cols-1 xl:grid-cols-2 gap-4 bg-app-bg/50 rounded-lg p-4 border border-app-border text-sm">
<button class="btn btn-error btn-sm btn-outline" type="submit">Отклонить</button> <div>
</form> <div class="text-xs text-app-textMuted font-bold mb-2 uppercase tracking-wider">Оригинальный текст</div>
<div class="whitespace-pre-wrap text-app-textMain/80 font-mono text-xs leading-relaxed max-h-[300px] overflow-y-auto">{{ p.raw_text }}</div>
</div>
<div>
<div class="text-xs text-app-textMuted font-bold mb-2 uppercase tracking-wider">AI Заметки / Резолюция</div>
<div class="text-app-textMain/80 whitespace-pre-wrap italic">{{ p.rewrite_notes or p.qualification_reason or "—" }}</div>
{% if p.editor_notes %}
<div class="mt-4 pt-4 border-t border-app-border text-app-warning">
<span class="font-bold block mb-1 uppercase tracking-wider text-xs">Заметка редактора:</span>
{{ p.editor_notes }}
</div>
{% endif %}
</div>
</div>
</details>
</div>
<!-- Actions -->
<div class="bg-app-bg border-t border-app-border p-3 flex flex-wrap items-center justify-between gap-3">
<div>
<!-- Если статус Ожидает (не принят и не отклонен), показываем быстрые кнопки -->
{% if p.editorial_status in ['pending', 'ai_review', 'reviewing'] %}
<div class="flex gap-2">
<form hx-post="/editor/{{ p.id }}/accept" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-primary btn-sm" type="submit">
<i data-lucide="check" class="w-4 h-4"></i> Принять сразу
</button>
</form>
<form hx-post="/editor/{{ p.id }}/reject" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML" class="m-0">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-ghost btn-sm text-app-error hover:bg-app-errorBg hover:text-app-error" type="submit" onclick="return confirm('Отклонить пост?')">
<i data-lucide="x" class="w-4 h-4"></i> В мусор
</button>
</form>
</div>
{% endif %}
</div>
<button class="btn btn-surface btn-sm" onclick="document.getElementById('editModal{{ p.id }}').showModal()">
<i data-lucide="edit" class="w-4 h-4"></i>
{% if p.editorial_status == 'accepted' or p.editorial_status == 'rejected' %}Редактировать снова{% else %}Открыть редактор{% endif %}
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal for Editing --> <!-- Modal for Editing -->
<dialog id="editModal{{ p.id }}" class="modal"> <dialog id="editModal{{ p.id }}" class="modal">
<div class="modal-box w-11/12 max-w-5xl rounded-xl"> <div class="bg-app-surface w-11/12 max-w-6xl rounded-xl shadow-2xl border border-app-borderFocus flex flex-col max-h-[90vh]">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <!-- Modal Header -->
</form> <div class="flex justify-between items-center p-4 border-b border-app-border">
<h3 class="font-bold text-lg mb-1">Пост #{{ p.id }}</h3> <div>
<div class="text-sm text-base-content/60 mb-6"> <h3 class="font-bold text-white text-lg">Редактирование поста #{{ p.id }}</h3>
<a href="{{ p.original_url }}" target="_blank" class="link link-hover">{{ p.source_name }}</a> &bull; {{ p.posted_at_fmt or p.created_at_fmt }} <div class="text-sm text-app-textMuted mt-1">
<a href="{{ p.original_url }}" target="_blank" class="text-blue-400 hover:underline">{{ p.source_name }}</a>
&bull; {{ p.posted_at_fmt or p.created_at_fmt }}
</div>
</div>
<form method="dialog">
<button class="btn btn-ghost btn-icon rounded-full text-app-textMuted hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button>
</form>
</div> </div>
<!-- HTMX form to submit edits --> <!-- HTMX form -->
<form hx-post="/editor/{{ p.id }}/save" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML" hx-encoding="multipart/form-data" class="flex flex-col md:flex-row gap-6"> <form hx-post="/editor/{{ p.id }}/save" hx-target="#post-container-{{ p.id }}" hx-swap="outerHTML" hx-encoding="multipart/form-data" class="flex-1 overflow-hidden flex flex-col">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<div class="w-full md:w-1/3 flex flex-col gap-4"> <div class="flex-1 overflow-y-auto p-4 md:p-6 flex flex-col lg:flex-row gap-6 bg-app-bg">
<div class="bg-base-200 rounded p-4 text-sm whitespace-pre-wrap max-h-64 overflow-y-auto border border-base-300"> <!-- Left Column: Context -->
<span class="font-bold block mb-1">Оригинал:</span> <div class="w-full lg:w-5/12 flex flex-col gap-4">
{{ p.raw_text }}
<!-- Original Text -->
<div class="bg-app-surface border border-app-border rounded-lg p-3 flex flex-col max-h-64">
<span class="text-xs font-bold text-app-textMuted uppercase tracking-wider mb-2">Оригинал:</span>
<div class="overflow-y-auto text-xs font-mono text-app-textMain/70 whitespace-pre-wrap leading-relaxed pr-2">
{{ p.raw_text }}
</div>
</div>
<!-- AI Rewrite -->
<div class="bg-app-surface border border-app-border rounded-lg p-3 flex flex-col max-h-64">
<span class="text-xs font-bold text-blue-400 uppercase tracking-wider mb-2">AI-Предложение:</span>
<div class="overflow-y-auto text-xs font-mono text-app-textMain/90 whitespace-pre-wrap leading-relaxed pr-2">
{{ p.rewritten_text }}
</div>
</div>
<!-- Meta Inputs -->
<div class="bg-app-surface border border-app-border rounded-lg p-4 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Категория</label>
<select name="final_category" class="select select-sm w-full">
{% for category in categories %}
<option value="{{ category.name }}" data-tag="{{ category.tag }}" {% if p.review_category == category.name %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
{% set category_names = categories | map(attribute="name") | list %}
{% if p.review_category and p.review_category not in category_names %}
<option value="{{ p.review_category }}" selected>{{ p.review_category }} · старая</option>
{% endif %}
</select>
</div>
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Тег источника</label>
<input type="text" name="final_source_tag" value="{{ p.review_source_tag or p.source_tag or '' }}" class="input input-sm w-full">
</div>
</div>
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Заметка редактора</label>
<input type="text" name="editor_notes" value="{{ p.editor_notes or '' }}" class="input input-sm w-full text-app-warning placeholder:text-app-warning/50">
</div>
<div>
<label class="block text-xs font-bold text-app-textMuted mb-1">Доп. Медиа</label>
<input type="file" name="media_files" class="block w-full text-sm text-app-textMuted file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-app-border file:text-white hover:file:bg-app-borderFocus cursor-pointer border border-app-border rounded-md" accept="image/*,video/*,.pdf,.doc,.docx" multiple>
</div>
</div>
</div> </div>
<div class="bg-base-200 rounded p-4 text-sm whitespace-pre-wrap max-h-64 overflow-y-auto border border-base-300">
<span class="font-bold block mb-1">AI-рерайт:</span> <!-- Right Column: Edit Area -->
{{ p.rewritten_text }} <div class="w-full lg:w-7/12 flex flex-col">
</div> <label class="block text-sm font-bold text-white mb-2 flex items-center justify-between">
<div class="grid grid-cols-2 gap-2 text-sm bg-base-200 p-4 rounded border border-base-300"> <span>Финальный текст публикации</span>
<div class="text-base-content/60 font-bold">Оценка:</div><div>{{ p.qualification_score or "—" }}/10</div> </label>
<div class="text-base-content/60 font-bold">AI-Категория:</div><div>{{ p.rewrite_category or "—" }}</div> <textarea name="final_text" class="textarea flex-1 w-full font-sans text-[15px] leading-relaxed resize-none p-4" placeholder="Напишите идеальный пост здесь...">{{ p.review_text }}</textarea>
<div class="text-base-content/60 font-bold">Модель:</div><div class="truncate" title="{{ p.rewrite_model }}">{{ p.rewrite_model or "—" }}</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text">Добавить медиа</span></label>
<input type="file" name="media_files" class="file-input file-input-bordered file-input-sm w-full" accept="image/*,video/*,.pdf,.doc,.docx" multiple>
</div> </div>
</div> </div>
<div class="w-full md:w-2/3 flex flex-col gap-4"> <!-- Modal Footer -->
<div class="form-control"> <div class="p-4 bg-app-surface border-t border-app-border flex flex-wrap items-center justify-between gap-4">
<label class="label"><span class="label-text font-bold">Текст публикации</span></label> <form method="dialog" class="m-0">
<textarea name="final_text" class="textarea textarea-bordered h-64 font-mono text-sm leading-relaxed" placeholder="Текст...">{{ p.review_text }}</textarea> <button class="btn btn-ghost" type="button" onclick="document.getElementById('editModal{{ p.id }}').close()">Отмена</button>
</div> </form>
<div class="flex gap-2">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <button type="submit" name="action" value="save" class="btn btn-surface" onclick="document.getElementById('editModal{{ p.id }}').close()">
<div class="form-control"> <i data-lucide="save" class="w-4 h-4"></i> Сохранить как черновик
<label class="label"><span class="label-text font-bold">Категория</span></label> </button>
<select name="final_category" class="select select-bordered w-full"> <button type="submit" name="action" value="accept" class="btn btn-primary" onclick="document.getElementById('editModal{{ p.id }}').close()">
{% for category in categories %} <i data-lucide="send" class="w-4 h-4"></i> Сохранить и Опубликовать
<option value="{{ category.name }}" data-tag="{{ category.tag }}" {% if p.review_category == category.name %}selected{% endif %}>{{ category.name }}</option> </button>
{% endfor %}
{% set category_names = categories | map(attribute="name") | list %}
{% if p.review_category and p.review_category not in category_names %}
<option value="{{ p.review_category }}" selected>{{ p.review_category }} · старая</option>
{% endif %}
</select>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Тэг источника</span></label>
<input type="text" name="final_source_tag" value="{{ p.review_source_tag or p.source_tag or '' }}" class="input input-bordered w-full">
</div>
</div>
<div class="form-control">
<label class="label"><span class="label-text font-bold">Заметка редактора (для себя)</span></label>
<input type="text" name="editor_notes" value="{{ p.editor_notes or '' }}" class="input input-bordered w-full input-sm">
</div>
<div class="flex justify-end gap-2 mt-4 pt-4 border-t border-base-200">
<form method="dialog" class="inline">
<button class="btn btn-ghost" type="button" onclick="editModal{{ p.id }}.close()">Отмена</button>
</form>
<button type="submit" name="action" value="save" class="btn btn-outline btn-primary" onclick="editModal{{ p.id }}.close()">Сохранить правки</button>
<button type="submit" name="action" value="accept" class="btn btn-primary" onclick="editModal{{ p.id }}.close()">Принять к публикации</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop bg-black/60 backdrop-blur-sm">
<button>Закрыть</button> <button>Закрыть</button>
</form> </form>
</dialog> </dialog>
+86 -68
View File
@@ -1,78 +1,83 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold">Логи</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3 mb-2">
<div class="text-base-content/60 mt-1">Журнал действий и запросов админки. События старше 30 дней удаляются автоматически.</div> <i data-lucide="scroll-text" class="text-app-primary w-8 h-8"></i>
Логи
</h1>
<div class="text-app-textMuted text-sm">Журнал действий и запросов админки. События старше 30 дней удаляются автоматически.</div>
</div> </div>
<div class="card bg-base-100 shadow-sm border border-base-200 mb-8"> <div class="card overflow-hidden mb-8">
<div class="card-body p-4 bg-base-200/50 rounded-t-xl border-b border-base-200"> <div class="p-4 bg-app-surface border-b border-app-border">
<form method="get" action="/logs" class="flex flex-wrap items-end gap-4"> <form method="get" action="/logs" class="flex flex-wrap items-end gap-4">
<div class="form-control flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label class="label"><span class="label-text font-bold">Поиск</span></label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Поиск</label>
<input name="q" value="{{ q }}" placeholder="action, entity, JSON" class="input input-bordered input-sm w-full"> <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>
<input name="q" value="{{ q }}" placeholder="action, entity, JSON" class="input w-full pl-9">
</div>
</div> </div>
<div class="form-control"> <div class="w-full sm:w-auto">
<label class="label"><span class="label-text font-bold">Действие</span></label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Действие</label>
<select name="action" class="select select-bordered select-sm"> <select name="action" class="select w-full sm:w-40">
<option value="">Все</option> <option value="">Все</option>
{% for item in actions %} {% for item in actions %}
<option value="{{ item.action }}" {% if action == item.action %}selected{% endif %}>{{ item.action }} · {{ item.count }}</option> <option value="{{ item.action }}" {% if action == item.action %}selected{% endif %}>{{ item.action }} ({{ item.count }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-control"> <div class="w-full sm:w-auto">
<label class="label"><span class="label-text font-bold">Пользователь</span></label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Пользователь</label>
<select name="actor_id" class="select select-bordered select-sm"> <select name="actor_id" class="select w-full sm:w-40">
<option value="0">Все</option> <option value="0">Все</option>
{% for item in actors %} {% for item in actors %}
<option value="{{ item.id }}" {% if actor_id == item.id %}selected{% endif %}>{{ item.login }} · {{ item.count }}</option> <option value="{{ item.id }}" {% if actor_id == item.id %}selected{% endif %}>{{ item.login }} ({{ item.count }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-control"> <div class="w-full sm:w-auto">
<label class="label"><span class="label-text font-bold">Сущность</span></label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Сущность</label>
<select name="entity_type" class="select select-bordered select-sm"> <select name="entity_type" class="select w-full sm:w-40">
<option value="">Все</option> <option value="">Все</option>
{% for item in entity_types %} {% for item in entity_types %}
<option value="{{ item.entity_type }}" {% if entity_type == item.entity_type %}selected{% endif %}>{{ item.entity_type }} · {{ item.count }}</option> <option value="{{ item.entity_type }}" {% if entity_type == item.entity_type %}selected{% endif %}>{{ item.entity_type }} ({{ item.count }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-control"> <div class="w-full sm:w-auto">
<label class="label"><span class="label-text font-bold">С даты</span></label> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Период</label>
<input type="date" name="date_from" value="{{ date_from }}" class="input input-bordered input-sm"> <div class="flex gap-2">
<input type="date" name="date_from" value="{{ date_from }}" class="input w-[130px]">
<input type="date" name="date_to" value="{{ date_to }}" class="input w-[130px]">
</div>
</div> </div>
<div class="form-control"> <div class="flex gap-2 w-full lg:w-auto">
<label class="label"><span class="label-text font-bold">По дату</span></label> <button class="btn btn-primary flex-1 lg:flex-none" type="submit">Найти</button>
<input type="date" name="date_to" value="{{ date_to }}" class="input input-bordered input-sm"> <a class="btn btn-surface flex-1 lg:flex-none border-app-border" href="/logs">Сбросить</a>
</div>
<div class="flex gap-2 w-full md:w-auto">
<button class="btn btn-primary btn-sm flex-1 md:flex-none" type="submit">Показать</button>
<a class="btn btn-outline btn-sm flex-1 md:flex-none" href="/logs">Сбросить</a>
</div> </div>
</form> </form>
</div> </div>
<div class="p-4 border-b border-base-200 flex justify-between items-center bg-base-100"> <div class="p-4 border-b border-app-border flex justify-between items-center bg-app-bg/50">
<div class="text-sm text-base-content/70"> <div class="text-sm text-app-textMuted flex items-center gap-2">
Всего записей: <span class="font-bold">{{ pagination.total }}</span> <i data-lucide="layers" class="w-4 h-4"></i>
Всего записей: <span class="font-bold text-white">{{ pagination.total }}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm">На странице:</span> <span class="text-sm text-app-textMuted">На странице:</span>
<form method="get" action="/logs" class="m-0"> <form method="get" action="/logs" class="m-0 flex items-center gap-2">
{% for item in preserved_query %} {% for item in preserved_query %}
{% if item.key != "per_page" %} {% if item.key != "per_page" %}
<input type="hidden" name="{{ item.key }}" value="{{ item.value }}"> <input type="hidden" name="{{ item.key }}" value="{{ item.value }}">
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<select name="per_page" class="select select-bordered select-sm" onchange="this.form.submit()"> <select name="per_page" class="select select-sm w-20 py-1" onchange="this.form.submit()">
{% 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 %}
@@ -81,46 +86,55 @@
</div> </div>
</div> </div>
<div class="card-body p-0 overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra w-full text-sm"> <table class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-base-200 text-base-content/70"> <thead class="bg-app-bg border-b border-app-border">
<tr> <tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th class="w-40">Время</th> <th class="px-4 py-3 w-40">Время</th>
<th class="w-32">Пользователь</th> <th class="px-4 py-3 w-32">Пользователь</th>
<th class="w-48">Действие</th> <th class="px-4 py-3 w-48">Действие</th>
<th class="w-48">Сущность</th> <th class="px-4 py-3 w-48">Сущность</th>
<th>Детали</th> <th class="px-4 py-3 min-w-[300px] w-full">Детали</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border text-xs">
{% for item in logs %} {% for item in logs %}
<tr class="hover"> <tr class="hover:bg-app-surfaceHover transition-colors group">
<td class="font-mono text-xs text-base-content/70">{{ item.created_at_fmt }}</td> <td class="px-4 py-3 font-mono text-app-textMuted text-[10px]">{{ item.created_at_fmt }}</td>
<td class="font-bold">{{ item.actor_label }}</td> <td class="px-4 py-3 font-bold text-white">{{ item.actor_label }}</td>
<td><span class="badge badge-outline badge-sm font-mono">{{ item.action }}</span></td> <td class="px-4 py-3"><span class="badge badge-neutral px-1.5 py-0.5 text-[10px] font-mono border-app-border">{{ item.action }}</span></td>
<td class="font-mono text-xs"> <td class="px-4 py-3 font-mono text-app-textMuted text-[10px]">
{{ item.entity_type }} {{ item.entity_type }}
{% if item.entity_id %} {% if item.entity_id %}
<span class="text-base-content/50">#{{ item.entity_id }}</span> <span class="text-white ml-1">#{{ item.entity_id }}</span>
{% endif %} {% endif %}
</td> </td>
<td> <td class="px-4 py-3 whitespace-normal">
{% if item.details_pretty %} {% if item.details_pretty %}
<div class="collapse collapse-arrow bg-base-200/50 rounded-lg border border-base-200"> <details class="group/json">
<input type="checkbox" /> <summary class="cursor-pointer select-none flex items-center gap-1.5 text-app-primary hover:text-white transition-colors">
<div class="collapse-title min-h-0 py-2 text-xs font-bold">JSON payload</div> <i data-lucide="chevron-right" class="w-4 h-4 transition-transform group-open/json:rotate-90"></i>
<div class="collapse-content border-t border-base-200 pt-2 pb-2"> <span class="text-[10px] font-bold uppercase tracking-wider">JSON Payload</span>
<pre class="bg-base-300 p-2 rounded text-xs font-mono overflow-x-auto">{{ item.details_pretty }}</pre> </summary>
<div class="mt-2 pl-5">
<div class="bg-[#0f1115] border border-app-border rounded-lg p-3 max-h-64 overflow-auto">
<pre class="font-mono text-[10px] text-app-textMuted leading-relaxed">{{ item.details_pretty }}</pre>
</div>
</div> </div>
</div> </details>
{% else %} {% else %}
<span class="text-base-content/30"></span> <span class="text-app-textMuted/50"></span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="5" class="text-center p-8 text-base-content/50 italic">Записей нет.</td> <td colspan="5" class="px-4 py-12 text-center">
<div class="flex flex-col items-center justify-center text-app-textMuted">
<i data-lucide="search-x" class="w-12 h-12 mb-3 text-app-borderFocus"></i>
<p class="text-sm">Записей по заданным фильтрам не найдено</p>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -128,13 +142,17 @@
</div> </div>
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="card-body p-4 border-t border-base-200 flex justify-center"> <div class="p-4 border-t border-app-border flex justify-center bg-app-bg/50">
<div class="join"> <div class="tabs p-1">
<a class="join-item btn btn-sm {% if pagination.page == 1 %}btn-disabled{% endif %}" href="?page=1">«</a> <a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page == 1 %}pointer-events-none opacity-50{% endif %}" href="?page=1{% for k,v in preserved_query %}{% if k != 'page' %}&{{ k }}={{ v }}{% endif %}{% endfor %}"><i data-lucide="chevrons-left" class="w-4 h-4"></i></a>
<a class="join-item btn btn-sm {% if pagination.page == 1 %}btn-disabled{% endif %}" href="?page={{ pagination.page - 1 }}"></a> <a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page == 1 %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.page - 1 }}{% for k,v in preserved_query %}{% if k != 'page' %}&{{ k }}={{ v }}{% endif %}{% endfor %}"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
<button class="join-item btn btn-sm btn-disabled">Стр. {{ pagination.page }} из {{ pagination.pages }}</button>
<a class="join-item btn btn-sm {% if pagination.page >= pagination.pages %}btn-disabled{% endif %}" href="?page={{ pagination.page + 1 }}"></a> <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">
<a class="join-item btn btn-sm {% if pagination.page >= pagination.pages %}btn-disabled{% endif %}" href="?page={{ pagination.pages }}">»</a> {{ pagination.page }} из {{ pagination.pages }}
</span>
<a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page >= pagination.pages %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.page + 1 }}{% for k,v in preserved_query %}{% if k != 'page' %}&{{ k }}={{ v }}{% endif %}{% endfor %}"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
<a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page >= pagination.pages %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.pages }}{% for k,v in preserved_query %}{% if k != 'page' %}&{{ k }}={{ v }}{% endif %}{% endfor %}"><i data-lucide="chevrons-right" class="w-4 h-4"></i></a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
+7 -4
View File
@@ -1,10 +1,13 @@
{% extends "v2/base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="mb-6 flex justify-between items-end"> <div class="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div> <div>
<h1 class="text-3xl font-bold mb-1">Raw-посты</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
<div class="text-base-content/60">Исходный пост, этап обработки и AI-оценка.</div> <i data-lucide="inbox" class="text-app-primary w-8 h-8"></i>
Сырые посты
</h1>
<div class="text-app-textMuted mt-1 text-sm">Этапы обработки исходных постов и AI-квалификация.</div>
</div> </div>
</div> </div>
+144 -112
View File
@@ -1,142 +1,174 @@
<div class="flex flex-col lg:flex-row gap-6"> <div class="flex flex-col xl:flex-row gap-6">
<!-- Sidebar Filters --> <!-- Sidebar Filters -->
<aside class="w-full lg:w-72 flex-shrink-0"> <aside class="w-full xl:w-72 flex-shrink-0">
<div class="card bg-base-100 shadow-sm border border-base-200"> <div class="card p-4 sticky top-6">
<div class="card-body p-4"> <form id="filter-form" hx-get="/raw" hx-target="#content-area" hx-push-url="true" hx-trigger="submit, change delay:300ms">
<form hx-get="/raw" hx-target="#content-area" hx-push-url="true" hx-trigger="submit, change delay:300ms"> <input type="hidden" name="sort" value="{{ sort }}">
<input type="hidden" name="sort" value="{{ sort }}">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-6">
<h2 class="card-title text-lg">Фильтры</h2> <h2 class="text-sm font-bold text-app-textMuted uppercase tracking-wider">Фильтры</h2>
<a href="/raw" class="text-sm link link-hover text-base-content/60" hx-boost="true" hx-target="#content-area">Сбросить</a> <a href="/raw" class="text-xs text-app-primary hover:text-app-primaryHover transition-colors" hx-boost="true" hx-target="#content-area">Сбросить</a>
</div> </div>
<div class="form-control w-full mb-4"> <div class="space-y-6">
<label class="label"><span class="label-text font-bold">Оценка AI</span></label> <!-- Score -->
<div>
<label class="block text-xs font-semibold text-app-textMain mb-2">Оценка AI</label>
<div class="flex gap-2"> <div class="flex gap-2">
<input type="number" name="score_min" placeholder="От" value="{{ score_min }}" class="input input-bordered input-sm w-full" /> <input type="number" name="score_min" placeholder="От" value="{{ score_min }}" class="input w-full" />
<input type="number" name="score_max" placeholder="До" value="{{ score_max }}" class="input input-bordered input-sm w-full" /> <input type="number" name="score_max" placeholder="До" value="{{ score_max }}" class="input w-full" />
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Dates -->
<label class="label"><span class="label-text font-bold">Дата поста</span></label> <div>
<div class="flex flex-col gap-2"> <label class="block text-xs font-semibold text-app-textMain mb-2">Дата поста</label>
<input type="date" name="date_from" value="{{ date_from }}" class="input input-bordered input-sm w-full" /> <div class="space-y-2">
<input type="date" name="date_to" value="{{ date_to }}" class="input input-bordered input-sm w-full" /> <input type="date" name="date_from" value="{{ date_from }}" class="input w-full" />
<input type="date" name="date_to" value="{{ date_to }}" class="input w-full" />
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Qualification Status -->
<label class="label"><span class="label-text font-bold">Квалификация</span></label> <div>
<div class="bg-base-200 rounded-lg p-2 max-h-48 overflow-y-auto"> <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">
{% for item in facets.qualification_statuses %} {% for item in facets.qualification_statuses %}
<label class="label cursor-pointer justify-start gap-2 p-1"> <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 checkbox-sm checkbox-primary" {% 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 %} />
<span class="label-text flex-1">{{ item.value }}</span> <span class="text-sm text-app-textMain flex-1 group-hover:text-white">{{ item.value }}</span>
<span class="badge badge-sm badge-ghost">{{ item.count }}</span> <span class="badge badge-neutral bg-transparent border-none">{{ item.count }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-control w-full mb-4"> <!-- Rewrite Status -->
<label class="label"><span class="label-text font-bold">Рерайт</span></label> <div>
<div class="bg-base-200 rounded-lg p-2 max-h-48 overflow-y-auto"> <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">
{% for item in facets.rewrite_statuses %} {% for item in facets.rewrite_statuses %}
<label class="label cursor-pointer justify-start gap-2 p-1"> <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 checkbox-sm checkbox-primary" {% 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 %} />
<span class="label-text flex-1">{{ item.value }}</span> <span class="text-sm text-app-textMain flex-1 group-hover:text-white">{{ item.value }}</span>
<span class="badge badge-sm badge-ghost">{{ item.count }}</span> <span class="badge badge-neutral bg-transparent border-none">{{ item.count }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<button class="btn btn-primary w-full mt-4" type="submit">Показать</button> <button class="btn btn-primary w-full" type="submit">Применить фильтры</button>
</form> </div>
</div> </form>
</div> </div>
</aside> </aside>
<!-- Main Content (Table) --> <!-- Main Content (Table) -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0 flex flex-col gap-6">
<div class="card bg-base-100 shadow-sm border border-base-200"> <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-app-surface border border-app-border p-3 rounded-xl shadow-sm">
<div class="card-body p-0"> <div class="text-sm text-app-textMuted flex items-center gap-2">
<i data-lucide="layers" class="w-4 h-4"></i>
<!-- Toolbar --> Всего: <span class="font-bold text-white">{{ pagination.total }}</span>
<div class="p-4 border-b border-base-200 flex justify-between items-center bg-base-100/50"> </div>
<div class="text-sm text-base-content/70"> <div class="flex items-center gap-2">
Всего: <span class="font-bold">{{ pagination.total }}</span> <span class="text-sm text-app-textMuted">На странице:</span>
</div> <select name="per_page" class="select select-sm w-auto py-1" form="filter-form">
<!-- Pagination top (optional, or per page select) --> {% for n in [25,50,100,200] %}
<div class="flex items-center gap-2"> <option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option>
<span class="text-sm">На странице:</span> {% endfor %}
<select name="per_page" class="select select-bordered select-sm" form="pagination-form" hx-get="/raw" hx-target="#content-area" hx-include="[name='date_from'], [name='date_to']"> </select>
{% for n in [25,50,100,200] %}
<option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra w-full text-sm">
<thead>
<tr class="bg-base-200 text-base-content">
<th class="w-16">#</th>
<th class="w-48">Источник</th>
<th class="w-32">Этап</th>
<th>Пост</th>
<th class="w-32 text-center">AI</th>
</tr>
</thead>
<tbody>
{% for p in posts %}
<tr class="hover">
<td class="font-bold">
<a href="/raw/{{ p.id }}" class="link link-primary">{{ p.id }}</a>
</td>
<td>
<a href="{{ p.original_url }}" target="_blank" class="font-medium hover:underline">{{ p.source_name }}</a>
<div class="text-xs text-base-content/60 mt-1">{{ p.source_platform }} · #{{ p.source_tag or "—" }}</div>
{% if p.posted_at_fmt %}<div class="text-xs text-base-content/60 mt-1">{{ p.posted_at_fmt }}</div>{% endif %}
</td>
<td>
<div class="badge badge-sm {% if p.stage.class == 'ok' %}badge-success{% elif p.stage.class == 'bad' %}badge-error{% elif p.stage.class == 'warn' %}badge-warning{% else %}badge-ghost{% endif %} gap-1">
{{ p.stage.label }}
</div>
{% if p.error_reason %}<div class="text-xs text-error mt-1">{{ p.error_reason }}</div>{% endif %}
</td>
<td class="max-w-md">
<div class="prose prose-sm leading-snug">
{{ p.raw_text[:200] }}{% if p.raw_text|length > 200 %}...{% endif %}
</div>
</td>
<td class="text-center">
<div class="flex flex-col items-center gap-1">
<span class="text-lg font-bold {% if p.ai_badge.class == 'ok' %}text-success{% elif p.ai_badge.class == 'bad' %}text-error{% elif p.ai_badge.class == 'warn' %}text-warning{% endif %}">
{{ p.ai_badge.label }}
</span>
<span class="text-xs text-base-content/60">{{ p.ai_badge.sublabel }}</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination Bottom -->
<div class="p-4 border-t border-base-200 flex justify-end">
<div class="join">
<button class="join-item btn btn-sm" form="filter-form" name="page" value="{{ pagination.page - 1 }}" {% if not pagination.has_prev %}disabled{% endif %}>« Пред</button>
<button class="join-item btn btn-sm btn-active">Стр. {{ pagination.page }} из {{ pagination.pages }}</button>
<button class="join-item btn btn-sm" form="filter-form" name="page" value="{{ pagination.page + 1 }}" {% if not pagination.has_next %}disabled{% endif %}>След »</button>
</div>
</div>
</div> </div>
</div> </div>
<div class="card overflow-hidden">
<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">Этап</th>
<th class="px-4 py-3 min-w-[300px] w-full">Пост</th>
<th class="px-4 py-3 text-center">AI Оценка</th>
</tr>
</thead>
<tbody class="divide-y divide-app-border">
{% for p in posts %}
<tr class="hover:bg-app-surfaceHover transition-colors group">
<td class="px-4 py-3 font-mono text-xs">
<a href="/raw/{{ p.id }}" class="text-app-primary hover:text-white transition-colors">#{{ p.id }}</a>
</td>
<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>
<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-blue-400 font-mono">#{{ p.source_tag or "—" }}</span>
</div>
{% if p.posted_at_fmt %}
<div class="text-[10px] text-app-textMuted mt-1">{{ p.posted_at_fmt }}</div>
{% endif %}
</td>
<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
{% if p.stage.class == 'ok' %}bg-app-successBg text-app-success border-app-success/20
{% elif p.stage.class == 'bad' %}bg-app-errorBg text-app-error border-app-error/20
{% elif p.stage.class == 'warn' %}bg-app-warningBg text-app-warning border-app-warning/20
{% else %}bg-app-bg text-app-textMuted border-app-border{% endif %}">
{{ p.stage.label }}
</div>
{% if p.error_reason %}
<div class="text-[10px] text-app-error mt-1 max-w-[200px] whitespace-normal leading-tight">{{ p.error_reason }}</div>
{% endif %}
</td>
<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">
{{ p.raw_text }}
</div>
</td>
<td class="px-4 py-3 text-center">
<div class="flex flex-col items-center gap-1">
<span class="text-lg font-bold font-mono
{% if p.ai_badge.class == 'ok' %}text-app-success
{% elif p.ai_badge.class == 'bad' %}text-app-error
{% elif p.ai_badge.class == 'warn' %}text-app-warning
{% 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>
</tr>
{% endfor %}
{% if not posts %}
<tr>
<td colspan="5" class="px-4 py-12 text-center">
<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>
<p class="text-sm">Нет данных по заданным фильтрам</p>
</div>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if pagination.pages > 1 %}
<div class="flex justify-center mt-4">
<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 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>
<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 }}
</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 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>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
+30 -24
View File
@@ -1,44 +1,50 @@
<tr class="hover" id="source-row-{{ s.id }}"> <tr class="hover:bg-app-surfaceHover transition-colors" id="source-row-{{ s.id }}">
<td class="font-bold text-base-content/50">{{ s.id }}</td> <td class="px-4 py-3 font-mono text-[10px] text-app-textMuted">{{ s.id }}</td>
<td class="uppercase text-xs tracking-widest font-bold text-base-content/60">{{ s.platform }}</td> <td class="px-4 py-3 uppercase text-[10px] tracking-widest font-bold text-app-textMuted">{{ s.platform }}</td>
<td> <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-base">{{ s.name }}</span> <span class="font-bold text-app-textMain">{{ s.name }}</span>
{% if s.active %} {% if s.active %}
<span class="badge badge-success badge-sm" title="Включен">ON</span> <span class="badge badge-success px-1.5 py-0.5 text-[10px]" title="Включен">ON</span>
{% else %} {% else %}
<span class="badge badge-error badge-sm" title="Выключен">OFF</span> <span class="badge badge-error px-1.5 py-0.5 text-[10px]" title="Выключен">OFF</span>
{% endif %} {% endif %}
</div> </div>
<div class="text-xs font-mono text-base-content/50 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>
<td class="font-mono text-xs text-base-content/70">#{{ s.tag or "—" }}</td> <td class="px-4 py-3 font-mono text-[10px] text-blue-400">#{{ s.tag or "—" }}</td>
<td> <td class="px-4 py-3">
<a href="{{ s.url }}" target="_blank" class="link link-primary truncate inline-block max-w-[12rem]" title="{{ s.url }}">{{ s.url }}</a> <a href="{{ s.url }}" target="_blank" class="text-app-textMuted hover:text-white hover:underline truncate inline-block max-w-[12rem]" title="{{ s.url }}">{{ s.url }}</a>
</td> </td>
<td> <td class="px-4 py-3">
<div class="flex flex-col gap-1"> <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-ghost{% endif %} badge-sm">{{ s.status }}</span> <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>
{% if s.status_msg %} {% if s.status_msg %}
<span class="text-xs text-base-content/60 truncate max-w-[10rem]" title="{{ s.status_msg }}">{{ s.status_msg }}</span> <span class="text-[10px] text-app-textMuted truncate max-w-[10rem]" title="{{ s.status_msg }}">{{ s.status_msg }}</span>
{% endif %} {% endif %}
</div> </div>
</td> </td>
<td class="text-right"> <td class="px-4 py-3 text-right">
<div class="font-bold">{{ s.posts_count }}</div> <div class="font-bold text-app-textMain">{{ s.posts_count }}</div>
<div class="text-xs text-success" title="За последние 24 часа">+{{ s.posts_24h }}</div> <div class="text-[10px] text-app-success" title="За последние 24 часа">+{{ s.posts_24h }}</div>
</td> </td>
<td class="text-right text-xs text-base-content/70">{{ s.last_parsed_at_fmt or "Никогда" }}</td> <td class="px-4 py-3 text-right text-[10px] text-app-textMuted">{{ s.last_parsed_at_fmt or "Никогда" }}</td>
<td> <td class="px-4 py-3 text-center">
<div class="flex justify-center gap-1"> <div class="flex justify-center items-center gap-1">
<a href="/sources/{{ s.id }}/edit" class="btn btn-ghost btn-xs btn-square" title="Править"></a> <a href="/sources/{{ s.id }}/edit" class="btn btn-ghost btn-icon w-8 h-8 rounded-md hover:bg-app-surface text-app-textMuted hover:text-white" title="Править">
<i data-lucide="edit-2" class="w-4 h-4"></i>
</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-xs btn-square" type="submit" title="{% if s.active %}Выключить{% else %}Включить{% endif %}"></button> <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>
</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">
<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-xs btn-square text-error hover:bg-error hover:text-error-content" type="submit" title="Удалить">×</button> <button class="btn btn-ghost btn-icon w-8 h-8 rounded-md hover:bg-app-errorBg text-app-textMuted hover:text-app-error" type="submit" title="Удалить">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</form> </form>
</div> </div>
</td> </td>
+113 -86
View File
@@ -1,109 +1,122 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="mb-6"> <div class="mb-8">
<h1 class="text-3xl font-bold">Источники</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3 mb-2">
<div class="text-base-content/60 mt-1">VK-источники для парсинга. Название идёт в prompt, тэг — в будущие хэштеги.</div> <i data-lucide="database" class="text-app-primary w-8 h-8"></i>
Источники
</h1>
<div class="text-app-textMuted text-sm">VK-источники для парсинга. Название идёт в prompt, тэг — в будущие хэштеги.</div>
</div> </div>
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200 mb-6 {% if source_preview %}collapse-open{% endif %}"> <details class="card mb-8 group/details" {% if source_preview %}open{% endif %}>
<input type="checkbox" {% if source_preview %}checked{% endif %} /> <summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors list-none">
<div class="collapse-title text-lg font-bold"> <div class="flex items-center gap-3">
Добавить источники <div class="w-8 h-8 rounded-lg bg-app-primary/20 text-app-primary flex items-center justify-center">
</div> <i data-lucide="plus" class="w-5 h-5"></i>
<div class="collapse-content border-t border-base-200 pt-4"> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <span class="text-lg font-bold text-white">Добавить источники</span>
</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 border-t border-app-border bg-app-bg/30">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-10">
<!-- Single Add --> <!-- Single Add -->
<div> <div>
<h3 class="font-bold mb-4">Добавить один</h3> <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>
<form method="post" action="/sources/new" class="flex flex-col gap-3"> <form method="post" action="/sources/new" class="flex flex-col gap-4">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="platform" value="vk"> <input type="hidden" name="platform" value="vk">
<input type="hidden" name="priority" value="100"> <input type="hidden" name="priority" value="100">
<div class="form-control"> <div>
<label class="label"><span class="label-text">Название</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1">Название</label>
<input name="name" class="input input-bordered input-sm" placeholder="Можно оставить как в VK"> <input name="name" class="input" placeholder="Можно оставить пустым, подтянется из VK">
</div> </div>
<div class="form-control"> <div>
<label class="label"><span class="label-text">Тэг</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1">Тэг (для хэштега)</label>
<input name="tag" class="input input-bordered input-sm" placeholder="academy_gear"> <input name="tag" class="input" placeholder="academy_gear">
</div> </div>
<div class="form-control"> <div>
<label class="label"><span class="label-text">Ссылка</span></label> <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 input-bordered input-sm" placeholder="https://vk.com/academy_gear" required> <input name="url" class="input" placeholder="https://vk.com/academy_gear" required>
</div> </div>
<div class="form-control"> <label class="flex items-center gap-3 mt-2 cursor-pointer group">
<label class="label cursor-pointer justify-start gap-2"> <input name="active" type="checkbox" class="checkbox" checked>
<input name="active" type="checkbox" class="checkbox checkbox-primary checkbox-sm" checked> <span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Включить сразу после добавления</span>
<span class="label-text">Включить сразу</span> </label>
</label>
</div>
<button class="btn btn-primary btn-sm mt-2" type="submit">Добавить</button> <button class="btn btn-primary mt-2 self-start" type="submit">
<i data-lucide="plus" class="w-4 h-4"></i> Добавить источник
</button>
</form> </form>
</div> </div>
<!-- Bulk Add --> <!-- Bulk Add -->
<div> <div>
<h3 class="font-bold mb-4">Массовое добавление</h3> <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>
<form method="post" action="/sources/preview" class="flex flex-col gap-3"> <form method="post" action="/sources/preview" class="flex flex-col gap-4">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="q" value="{{ q }}"> <input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="status_filter" value="{{ status_filter }}"> <input type="hidden" name="status_filter" value="{{ status_filter }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}"> <input type="hidden" name="per_page" value="{{ pagination.per_page }}">
<div class="form-control"> <div>
<label class="label"><span class="label-text">Список (Название Тэг Ссылка)</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1 flex justify-between">
<textarea name="bulk_text" class="textarea textarea-bordered font-mono text-xs" rows="5" placeholder="Academy Gear academy_gear https://vk.com/academy_gear&#10;A2 Technologies a2technologies https://vk.com/a2technologies">{{ bulk_text }}</textarea> <span>Список (Название Тэг Ссылка)</span>
</div> <span class="text-[10px] text-app-textMuted/60 font-normal">По 1 на строку</span>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-2">
<input name="bulk_active" type="checkbox" class="checkbox checkbox-primary checkbox-sm" {% if bulk_active %}checked{% endif %}>
<span class="label-text">Включить после добавления</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>
</div> </div>
<button class="btn btn-outline btn-sm mt-2" type="submit">Проверить список</button> <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> </form>
</div> </div>
</div> </div>
{% if source_preview %} {% if source_preview %}
<div class="mt-8 pt-4 border-t border-base-200"> <div class="mt-8 pt-6 border-t border-app-border">
<h3 class="font-bold mb-4">Предпросмотр массового добавления</h3> <h3 class="font-bold text-white mb-4 flex items-center gap-2"><i data-lucide="eye" class="w-4 h-4 text-app-primary"></i> Результаты проверки</h3>
<div class="overflow-x-auto bg-base-200 rounded-lg">
<table class="table table-sm"> <div class="overflow-x-auto bg-app-bg border border-app-border rounded-xl">
<thead> <table class="w-full text-left text-sm whitespace-nowrap">
<tr> <thead class="border-b border-app-border bg-app-surface">
<th># строки</th> <tr class="text-app-textMuted font-semibold tracking-wide uppercase text-[10px]">
<th>Название</th> <th class="px-4 py-3"># Стр.</th>
<th>Тэг</th> <th class="px-4 py-3">Название</th>
<th>Ссылка</th> <th class="px-4 py-3">Тэг</th>
<th>VK ID</th> <th class="px-4 py-3">Ссылка</th>
<th>Статус</th> <th class="px-4 py-3">VK ID</th>
<th class="px-4 py-3">Статус</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border text-xs">
{% for item in source_preview %} {% for item in source_preview %}
<tr> <tr class="hover:bg-app-surfaceHover transition-colors">
<td>{{ item.line_no }}</td> <td class="px-4 py-3 text-app-textMuted font-mono">{{ item.line_no }}</td>
<td>{{ item.name or "—" }}</td> <td class="px-4 py-3 font-semibold text-app-textMain">{{ item.name or "—" }}</td>
<td class="font-mono text-xs">#{{ item.tag or "—" }}</td> <td class="px-4 py-3 font-mono text-blue-400">#{{ item.tag or "—" }}</td>
<td><a href="{{ item.url }}" target="_blank" class="link">{{ item.url }}</a></td> <td class="px-4 py-3"><a href="{{ item.url }}" target="_blank" class="text-app-textMuted hover:text-white hover:underline">{{ item.url }}</a></td>
<td class="font-mono text-xs text-base-content/70"> <td class="px-4 py-3 font-mono text-app-textMuted text-[10px]">
{{ item.external_id }}<br> {{ item.external_id }}<br>
{{ item.external_owner_id or "" }} {{ item.external_owner_id or "" }}
</td> </td>
<td> <td class="px-4 py-3">
{% if item.ok %} {% if item.ok %}
<span class="badge badge-success badge-sm">Готов</span> <span class="badge badge-success">Готов</span>
{% else %} {% else %}
<span class="badge badge-error badge-sm" title="{{ item.error }}">Ошибка</span> <span class="badge badge-error" title="{{ item.error }}">Ошибка</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@@ -112,43 +125,57 @@
</table> </table>
</div> </div>
<form method="post" action="/sources/bulk" class="mt-4 flex items-center gap-4"> <form method="post" action="/sources/bulk" class="mt-6 flex flex-col sm:flex-row items-center gap-4 bg-app-warningBg border border-app-warning/20 p-4 rounded-xl">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="bulk_payload" value='{{ source_preview|tojson }}'> <input type="hidden" name="bulk_payload" value='{{ source_preview|tojson }}'>
<button class="btn btn-primary btn-sm" type="submit">Добавить прошедшие проверку</button> <button class="btn btn-primary" type="submit">
<span class="text-xs text-base-content/60">Строки с ошибками и дубли будут пропущены.</span> <i data-lucide="check-circle" class="w-4 h-4"></i> Добавить прошедшие проверку
</button>
<div class="text-sm text-app-warning flex items-center gap-2">
<i data-lucide="info" class="w-4 h-4"></i>
<span>Строки с ошибками и дубликаты будут автоматически пропущены.</span>
</div>
</form> </form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </details>
<div class="card bg-base-100 shadow-sm border border-base-200 mb-6"> <div class="card mb-6">
<div class="card-body p-4 flex flex-col md:flex-row gap-4 justify-between items-end bg-base-200/50 rounded-t-xl"> <div class="p-4 border-b border-app-border bg-app-bg/50">
<form class="flex-1 flex flex-wrap gap-4 items-end w-full" hx-get="/sources" hx-target="#content-area" hx-push-url="true"> <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">
<div class="form-control w-full max-w-xs"> <div class="flex flex-col sm:flex-row gap-4 flex-1">
<label class="label"><span class="label-text font-bold">Поиск</span></label> <div class="w-full sm:w-72">
<input name="q" value="{{ q }}" placeholder="Название, ссылка, id" class="input input-bordered input-sm w-full"> <label class="block text-xs font-bold text-app-textMuted mb-1">Поиск</label>
<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>
<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 class="form-control"> <div class="flex items-center gap-2 mt-4 md:mt-0 w-full md:w-auto">
<label class="label"><span class="label-text font-bold">Статус</span></label> {% include "table_tools.html" ignore missing %}
<select name="status_filter" class="select select-bordered select-sm">
<option value="">Все</option>
{% for st in ["new","ok","paused","error"] %}
<option value="{{ st }}" {% if status_filter == st %}selected{% endif %}>{{ st }}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary btn-sm" type="submit">Найти</button>
<div class="w-full">
{% include "table_tools.html" %}
</div> </div>
</form> </form>
</div> </div>
<div class="card-body p-0" id="content-area"> <div id="content-area">
{% include "sources_content.html" %} {% include "sources_content.html" %}
</div> </div>
</div> </div>
@@ -1,42 +1,29 @@
<div class="p-4 border-b border-base-200 flex justify-between items-center bg-base-100/50">
<div class="text-sm text-base-content/70">
Всего: <span class="font-bold">{{ pagination.total }}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm">На странице:</span>
<form id="pagination-form" hx-get="/sources" hx-target="#content-area" hx-push-url="true" class="m-0">
<input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="status_filter" value="{{ status_filter }}">
<select name="per_page" class="select select-bordered select-sm" onchange="this.form.requestSubmit()">
{% for n in [25,50,100,200] %}
<option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</form>
</div>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra w-full text-sm"> <table class="w-full text-left text-sm whitespace-nowrap">
<thead> <thead class="bg-app-bg border-b border-app-border">
<tr class="bg-base-200 text-base-content"> <tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th class="w-16">#</th> <th class="px-4 py-3 w-16">#</th>
<th class="w-24">Площадка</th> <th class="px-4 py-3 w-24">Платформа</th>
<th>Название</th> <th class="px-4 py-3">Название</th>
<th class="w-32">Тэг</th> <th class="px-4 py-3 w-32">Тэг</th>
<th class="w-48">Ссылка</th> <th class="px-4 py-3 w-48">Ссылка</th>
<th class="w-32">Статус</th> <th class="px-4 py-3 w-32">Статус</th>
<th class="w-24 text-right">Посты</th> <th class="px-4 py-3 w-24 text-right">Посты</th>
<th class="w-40 text-right">Последний парсинг</th> <th class="px-4 py-3 w-40 text-right">Последний парсинг</th>
<th class="w-24 text-center"></th> <th class="px-4 py-3 w-32 text-center">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border text-xs">
{% for s in sources %} {% for s in sources %}
{% include "source_row.html" %} {% include "source_row.html" %}
{% else %} {% else %}
<tr> <tr>
<td colspan="9" class="text-center p-8 text-base-content/50">Нет источников, удовлетворяющих фильтрам.</td> <td colspan="9" class="px-4 py-12 text-center">
<div class="flex flex-col items-center justify-center text-app-textMuted">
<i data-lucide="database" class="w-12 h-12 mb-3 text-app-borderFocus"></i>
<p class="text-sm">Нет источников, удовлетворяющих фильтрам.</p>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -45,13 +32,17 @@
<!-- Pagination bottom --> <!-- Pagination bottom -->
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="flex justify-center p-4 border-t border-base-200"> <div class="flex justify-center p-4 border-t border-app-border">
<div class="join"> <div class="tabs p-1">
<button class="join-item btn btn-sm" form="pagination-form" name="page" value="1" {% if pagination.page == 1 %}disabled{% endif %}>«</button> <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 class="join-item btn btn-sm" form="pagination-form" name="page" value="{{ pagination.page - 1 }}" {% if pagination.page == 1 %}disabled{% endif %}></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 class="join-item btn btn-sm btn-disabled">Стр. {{ pagination.page }} из {{ pagination.pages }}</button>
<button class="join-item btn btn-sm" form="pagination-form" name="page" value="{{ pagination.page + 1 }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}></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">
<button class="join-item btn btn-sm" form="pagination-form" name="page" value="{{ pagination.pages }}" {% if pagination.page >= pagination.pages %}disabled{% endif %}>»</button> {{ pagination.page }} из {{ pagination.pages }}
</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 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>
</div> </div>
</div> </div>
{% endif %} {% endif %}
+71 -48
View File
@@ -1,44 +1,53 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold">Пользователи</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3 mb-2">
<div class="text-base-content/60 mt-1">Управление доступом к админ-панели.</div> <i data-lucide="users" class="text-app-primary w-8 h-8"></i>
Пользователи
</h1>
<div class="text-app-textMuted text-sm">Управление доступом к админ-панели.</div>
</div> </div>
<div class="card bg-base-100 shadow-sm border border-base-200 mb-8"> <div class="card overflow-hidden mb-8">
<div class="card-body p-0 overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra w-full text-sm"> <table class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-base-200 text-base-content/70"> <thead class="bg-app-bg border-b border-app-border">
<tr> <tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th class="w-16">ID</th> <th class="px-4 py-3 w-16">ID</th>
<th>Логин</th> <th class="px-4 py-3">Логин</th>
<th>Роль</th> <th class="px-4 py-3">Роль</th>
<th>Создан</th> <th class="px-4 py-3">Создан</th>
<th>Статус</th> <th class="px-4 py-3">Статус</th>
<th class="w-32 text-right">Действия</th> <th class="px-4 py-3 w-32 text-right">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border text-xs">
{% for u in users %} {% for u in users %}
<tr class="hover"> <tr class="hover:bg-app-surfaceHover transition-colors">
<td class="font-bold text-base-content/50">{{ u.id }}</td> <td class="px-4 py-3 font-mono text-app-textMuted">{{ u.id }}</td>
<td class="font-bold">{{ u.login }}</td> <td class="px-4 py-3 font-bold text-app-textMain">{{ u.login }}</td>
<td> <td class="px-4 py-3">
<span class="badge badge-outline badge-sm">{{ u.role }}</span> <span class="badge badge-neutral px-2 py-0.5">{{ u.role }}</span>
</td> </td>
<td class="text-xs text-base-content/70">{{ u.created_at_fmt }}</td> <td class="px-4 py-3 text-app-textMuted">{{ u.created_at_fmt }}</td>
<td> <td class="px-4 py-3">
{% if u.is_active %} {% if u.is_active %}
<span class="badge badge-success badge-sm gap-1"><span class="w-1.5 h-1.5 rounded-full bg-success-content"></span> Активен</span> <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>
{% else %} {% else %}
<span class="badge badge-error badge-sm gap-1"><span class="w-1.5 h-1.5 rounded-full bg-error-content"></span> Заблокирован</span> <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 %} {% endif %}
</td> </td>
<td class="text-right"> <td class="px-4 py-3 text-right">
{% if user.role == "admin" and user.id != u.id %} {% if user.role == "admin" and user.id != u.id %}
<form method="post" action="/users/{{ u.id }}/toggle" class="m-0 inline-block"> <form method="post" action="/users/{{ u.id }}/toggle" class="m-0 inline-block">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn btn-xs btn-outline" type="submit">{% if u.is_active %}Блокировать{% else %}Разблокировать{% endif %}</button> <button class="btn btn-xs {% if u.is_active %}btn-danger-outline{% else %}btn-primary-outline{% endif %}" type="submit">
{% if u.is_active %}Блокировать{% else %}Разблокировать{% endif %}
</button>
</form> </form>
{% endif %} {% endif %}
</td> </td>
@@ -48,48 +57,62 @@
</table> </table>
</div> </div>
{% if pagination.pages > 1 %} {% if pagination.pages > 1 %}
<div class="card-body p-4 border-t border-base-200 flex justify-center"> <div class="p-4 border-t border-app-border flex justify-center bg-app-bg/50">
<div class="join"> <div class="tabs p-1">
<a class="join-item btn btn-sm {% if pagination.page == 1 %}btn-disabled{% endif %}" href="?page=1">«</a> <a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page == 1 %}pointer-events-none opacity-50{% endif %}" href="?page=1"><i data-lucide="chevrons-left" class="w-4 h-4"></i></a>
<a class="join-item btn btn-sm {% if pagination.page == 1 %}btn-disabled{% endif %}" href="?page={{ pagination.page - 1 }}"></a> <a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page == 1 %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.page - 1 }}"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
<button class="join-item btn btn-sm btn-disabled">Стр. {{ pagination.page }} из {{ pagination.pages }}</button>
<a class="join-item btn btn-sm {% if pagination.page >= pagination.pages %}btn-disabled{% endif %}" href="?page={{ pagination.page + 1 }}"></a> <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">
<a class="join-item btn btn-sm {% if pagination.page >= pagination.pages %}btn-disabled{% endif %}" href="?page={{ pagination.pages }}">»</a> {{ pagination.page }} из {{ pagination.pages }}
</span>
<a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page >= pagination.pages %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.page + 1 }}"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
<a class="btn btn-sm btn-ghost w-10 h-10 p-0 rounded-md {% if pagination.page >= pagination.pages %}pointer-events-none opacity-50{% endif %}" href="?page={{ pagination.pages }}"><i data-lucide="chevrons-right" class="w-4 h-4"></i></a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if user.role == "admin" %} {% if user.role == "admin" %}
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200"> <details class="card group/details">
<input type="checkbox" /> <summary class="p-4 flex items-center justify-between cursor-pointer select-none hover:bg-app-surfaceHover transition-colors list-none">
<div class="collapse-title text-lg font-bold">Добавить пользователя</div> <div class="flex items-center gap-3">
<div class="collapse-content border-t border-base-200 pt-4"> <div class="w-8 h-8 rounded-lg bg-app-primary/20 text-app-primary flex items-center justify-center">
<form method="post" action="/users/create" class="flex flex-col md:flex-row gap-4 items-end"> <i data-lucide="user-plus" class="w-5 h-5"></i>
</div>
<span class="text-lg font-bold text-white">Добавить пользователя</span>
</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 border-t border-app-border bg-app-bg/30">
<form method="post" action="/users/create" class="flex flex-col md:flex-row gap-6 items-end">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<div class="form-control w-full"> <div class="w-full">
<label class="label"><span class="label-text font-bold">Логин</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1">Логин</label>
<input name="login" class="input input-bordered input-sm w-full" required> <input name="login" class="input w-full" required>
</div> </div>
<div class="form-control w-full"> <div class="w-full">
<label class="label"><span class="label-text font-bold">Пароль</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1">Пароль</label>
<input name="password" type="password" class="input input-bordered input-sm w-full" required minlength="10" placeholder="Минимум 10 символов"> <input name="password" type="password" class="input w-full" required minlength="10" placeholder="Минимум 10 символов">
</div> </div>
<div class="form-control w-full max-w-xs"> <div class="w-full md:max-w-xs">
<label class="label"><span class="label-text font-bold">Роль</span></label> <label class="block text-xs font-bold text-app-textMuted mb-1">Роль</label>
<select name="role" class="select select-bordered select-sm w-full"> <select name="role" class="select w-full">
<option value="editor">Редактор (editor)</option> <option value="editor">Редактор (editor)</option>
<option value="viewer">Читатель (viewer)</option> <option value="viewer">Читатель (viewer)</option>
<option value="admin">Администратор (admin)</option> <option value="admin">Администратор (admin)</option>
</select> </select>
</div> </div>
<button class="btn btn-primary btn-sm" type="submit">Создать</button> <button class="btn btn-primary" type="submit">
<i data-lucide="plus" class="w-4 h-4"></i> Создать
</button>
</form> </form>
</div> </div>
</div> </details>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
+280 -211
View File
@@ -1,42 +1,49 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold">Система и Воркеры</h1> <h1 class="text-3xl font-bold tracking-tight text-white flex items-center gap-3 mb-2">
<div class="text-base-content/60 mt-1">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div> <i data-lucide="cpu" class="text-app-primary w-8 h-8"></i>
Система и Воркеры
</h1>
<div class="text-app-textMuted text-sm">Управление фоновыми процессами, категориями, расписанием и настройками AI.</div>
</div> </div>
<!-- Workers Panel --> <!-- Workers Panel -->
<div class="card bg-base-100 shadow-sm border border-base-200 mb-8"> <div class="card overflow-hidden mb-8">
<div class="card-body p-0 overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-zebra w-full text-sm"> <table class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-base-200"> <thead class="bg-app-bg border-b border-app-border">
<tr> <tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th>Воркер</th> <th class="px-4 py-3">Воркер</th>
<th>Состояние</th> <th class="px-4 py-3">Состояние</th>
<th>Heartbeat</th> <th class="px-4 py-3">Heartbeat</th>
<th>Статус</th> <th class="px-4 py-3">Статус</th>
<th>Текущая задача</th> <th class="px-4 py-3">Текущая задача</th>
<th>Действия</th> <th class="px-4 py-3">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border text-xs">
{% for w in workers %} {% for w in workers %}
<tr> <tr class="hover:bg-app-surfaceHover transition-colors">
<td class="font-mono font-bold">{{ w.name }}</td> <td class="px-4 py-3 font-mono font-bold text-white">{{ w.name }}</td>
<td> <td class="px-4 py-3">
{% if w.enabled %} {% if w.enabled %}
<span class="badge badge-success badge-sm gap-1"><span class="w-1.5 h-1.5 rounded-full bg-success-content"></span> Включен</span> <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>
{% else %} {% else %}
<span class="badge badge-error badge-sm gap-1"><span class="w-1.5 h-1.5 rounded-full bg-error-content"></span> Выключен</span> <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 %} {% endif %}
</td> </td>
<td class="text-xs text-base-content/70">{{ w.heartbeat_at or "—" }}</td> <td class="px-4 py-3 text-app-textMuted">{{ w.heartbeat_at or "—" }}</td>
<td class="text-xs max-w-xs truncate">{{ w.status or "—" }}</td> <td class="px-4 py-3 max-w-xs truncate text-app-textMain">{{ w.status or "—" }}</td>
<td class="font-mono text-xs text-base-content/60">{{ w.current_job_id or "—" }}</td> <td class="px-4 py-3 font-mono text-app-textMuted">{{ w.current_job_id or "—" }}</td>
<td> <td class="px-4 py-3">
<form method="post" action="/workers/{{ w.name }}/toggle" class="m-0"> <form method="post" action="/workers/{{ w.name }}/toggle" 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-xs btn-outline" type="submit">Переключить</button> <button class="btn btn-xs btn-surface" type="submit">Переключить</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -48,81 +55,97 @@
<div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-8"> <div class="grid grid-cols-1 xl:grid-cols-2 gap-8 mb-8">
<!-- Categories --> <!-- Categories -->
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200" open> <details class="card group/details" open>
<input type="checkbox" checked /> <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="collapse-title text-lg font-bold">Категории публикаций</div> <div class="flex items-center gap-2 font-bold text-lg text-white">
<div class="collapse-content border-t border-base-200 pt-4 flex flex-col gap-6"> <i data-lucide="tags" class="w-5 h-5 text-app-primary"></i> Категории публикаций
<div class="text-sm text-base-content/70"> </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, тэг используется в соцсетях. Название описывает категорию для AI, тэг используется в соцсетях.
</div> </div>
<form method="post" action="/categories/create" class="flex flex-col gap-3 bg-base-200 p-4 rounded-xl"> <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">Добавить категорию</div> <div class="font-bold text-sm text-white">Добавить категорию</div>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input name="name" class="input input-bordered input-sm w-full" placeholder="Название (для AI)" required> <div>
<input name="tag" class="input input-bordered input-sm w-full" placeholder="Тэг (для хэштегов)"> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Название (для AI)</label>
<input name="site_name" class="input input-bordered input-sm w-full" placeholder="На сайте" required> <input name="name" class="input w-full" required>
<input name="site_slug" class="input input-bordered input-sm w-full" placeholder="URL-slug" pattern="[a-z0-9-]+" 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>
<div class="flex items-center justify-between mt-2"> <div class="flex items-center justify-between mt-2">
<label class="cursor-pointer label gap-2 p-0"> <label class="cursor-pointer flex items-center gap-2 group">
<input type="checkbox" name="site_enabled" class="checkbox checkbox-primary checkbox-sm" checked> <input type="checkbox" name="site_enabled" class="checkbox" checked>
<span class="label-text">Публиковать</span> <span class="text-sm font-medium text-app-textMain group-hover:text-white transition-colors">Публиковать</span>
</label> </label>
<button class="btn btn-primary btn-sm" type="submit">Добавить</button> <button class="btn btn-primary btn-sm" type="submit">Добавить</button>
</div> </div>
</form> </form>
<div class="overflow-x-auto"> <div class="overflow-x-auto border border-app-border rounded-xl">
<table class="table table-xs w-full"> <table class="w-full text-left text-xs whitespace-nowrap bg-app-surface">
<thead class="bg-base-200 text-base-content/70"> <thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
<tr> <tr>
<th>Название / Тэг</th> <th class="px-3 py-2">Название / Тэг</th>
<th>Сайт / Slug</th> <th class="px-3 py-2">Сайт / Slug</th>
<th class="w-20 text-center">ID</th> <th class="px-3 py-2 text-center">ID</th>
<th>Статус</th> <th class="px-3 py-2">Статус</th>
<th class="w-16"></th> <th class="px-3 py-2 w-16"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="divide-y divide-app-border">
{% for c in categories %} {% for c in categories %}
<tr class="hover"> <tr class="hover:bg-app-surfaceHover transition-colors">
<td> <td class="px-3 py-2">
<form id="category-update-{{ c.id }}" method="post" action="/categories/{{ c.id }}/update" class="m-0"> <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 }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</form> </form>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1.5">
<input form="category-update-{{ c.id }}" name="name" value="{{ c.name }}" class="input input-bordered input-xs w-full"> <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-bordered input-xs w-full text-primary font-mono"> <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> </div>
</td> </td>
<td> <td class="px-3 py-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1.5">
<input form="category-update-{{ c.id }}" name="site_name" value="{{ c.site_name }}" class="input input-bordered input-xs w-full" required> <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-bordered input-xs w-full text-base-content/60 font-mono" 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> </div>
</td> </td>
<td class="text-center font-mono text-base-content/50">{{ c.sort_order }}</td> <td class="px-3 py-2 text-center font-mono text-app-textMuted text-[10px]">{{ c.sort_order }}</td>
<td> <td class="px-3 py-2">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-2">
<span class="badge {% if c.is_active %}badge-success{% else %}badge-ghost{% endif %} badge-sm w-full">{% if c.is_active %}Активна{% else %}Выкл{% endif %}</span> <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-1"> <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 checkbox-xs" {% if c.site_enabled %}checked{% endif %}> <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-xs">Сайт</span> <span class="text-[10px] font-medium text-app-textMuted">Сайт</span>
</label> </label>
</div> </div>
</td> </td>
<td> <td class="px-3 py-2">
<div class="flex flex-col gap-1 items-center"> <div class="flex flex-col gap-1 items-center">
<button form="category-update-{{ c.id }}" type="submit" class="btn btn-ghost btn-xs w-full" title="Сохранить"></button> <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"> <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 }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="btn btn-ghost btn-xs w-full" title="Вкл/Выкл">{% if c.is_active %}{% else %}{% endif %}</button> <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>
<form method="post" action="/categories/{{ c.id }}/delete" class="m-0 w-full" onsubmit="return confirm('Удалить категорию?');"> <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 }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button type="submit" class="btn btn-ghost btn-xs text-error hover:bg-error hover:text-error-content w-full" title="Удалить"></button> <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> </form>
</div> </div>
</td> </td>
@@ -132,189 +155,235 @@
</table> </table>
</div> </div>
</div> </div>
</div> </details>
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">
<!-- TG Schedule --> <!-- TG Schedule -->
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200" open> <details class="card group/details" open>
<input type="checkbox" checked /> <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="collapse-title text-lg font-bold">Расписание TG-постера</div> <div class="flex items-center gap-2 font-bold text-lg text-white">
<div class="collapse-content border-t border-base-200 pt-4 flex flex-col gap-4"> <i data-lucide="send" class="w-5 h-5 text-[#0088cc]"></i> Расписание TG-постера
<div class="text-sm text-base-content/70"> </div>
Время по Екб. Нужны: воркер <span class="font-mono bg-base-200 px-1 rounded text-xs">tg-poster</span> и настройка <span class="font-mono bg-base-200 px-1 rounded text-xs">tg_poster_enabled</span>. <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-4 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> </div>
<form method="post" action="/tg-poster-schedule/create" class="flex items-center gap-3 bg-base-200 p-3 rounded-xl"> <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">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="time" name="time" value="10:00" class="input input-bordered input-sm flex-1" required> <div class="flex-1 min-w-[120px]">
<input type="number" name="count" value="1" min="1" max="20" class="input input-bordered input-sm w-20" required> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время</label>
<label class="cursor-pointer label gap-2 p-0"> <input type="time" name="time" value="10:00" class="input input-sm w-full" required>
<input type="checkbox" name="enabled" class="checkbox checkbox-sm checkbox-primary" checked> </div>
</label> <div class="w-20">
<button class="btn btn-primary btn-sm" type="submit">Добавить</button> <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" required>
</div>
<div class="flex items-center h-8 px-2 border border-app-border rounded-lg bg-app-bg">
<label class="cursor-pointer flex items-center gap-2 group">
<input type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" checked>
<span class="text-xs font-medium text-app-textMain group-hover:text-white">Вкл</span>
</label>
</div>
<button class="btn btn-primary btn-sm h-8" type="submit">Добавить</button>
</form> </form>
<table class="table table-sm w-full"> <div class="overflow-x-auto border border-app-border rounded-xl">
<thead><tr><th>Время Екб</th><th>Постов</th><th>Статус</th><th></th></tr></thead> <table class="w-full text-left text-xs whitespace-nowrap bg-app-surface">
<tbody> <thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
{% for row in tg_schedule %} <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>
<tr class="hover"> </thead>
<td> <tbody class="divide-y divide-app-border">
<form id="tg-schedule-update-{{ row.id }}" method="post" action="/tg-poster-schedule/{{ row.id }}/update" class="m-0"> {% for row in tg_schedule %}
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <tr class="hover:bg-app-surfaceHover transition-colors">
</form> <td class="px-3 py-2">
<input form="tg-schedule-update-{{ row.id }}" type="time" name="time" value="{{ row.time }}" class="input input-bordered input-xs w-full" required> <form id="tg-schedule-update-{{ row.id }}" method="post" action="/tg-poster-schedule/{{ row.id }}/update" class="m-0">
</td> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<td><input form="tg-schedule-update-{{ row.id }}" type="number" name="count" value="{{ row.count }}" min="1" max="20" class="input input-bordered input-xs w-16" required></td> </form>
<td><label class="cursor-pointer"><input form="tg-schedule-update-{{ row.id }}" type="checkbox" name="enabled" class="toggle toggle-xs toggle-primary" {% if row.enabled %}checked{% endif %}></label></td> <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 class="flex gap-1"> </td>
<button form="tg-schedule-update-{{ row.id }}" type="submit" class="btn btn-ghost btn-xs btn-square"></button> <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>
<form method="post" action="/tg-poster-schedule/{{ row.id }}/delete" class="m-0"> <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>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <td class="px-3 py-2 flex gap-1 justify-center">
<button type="submit" class="btn btn-ghost btn-xs btn-square text-error"></button> <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> <form method="post" action="/tg-poster-schedule/{{ row.id }}/delete" class="m-0">
</td> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</tr> <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>
{% endfor %} </form>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </details>
<!-- VK Schedule --> <!-- VK Schedule -->
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200" open> <details class="card group/details" open>
<input type="checkbox" checked /> <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="collapse-title text-lg font-bold">Расписание VK-постера</div> <div class="flex items-center gap-2 font-bold text-lg text-white">
<div class="collapse-content border-t border-base-200 pt-4 flex flex-col gap-4"> <i data-lucide="layout-template" class="w-5 h-5 text-[#4680C2]"></i> Расписание VK-постера
<div class="text-sm text-base-content/70">
Время по Екб. Нужны: воркер <span class="font-mono bg-base-200 px-1 rounded text-xs">vk-poster</span> и <span class="font-mono bg-base-200 px-1 rounded text-xs">vk_poster_enabled</span>.
</div> </div>
<div><a class="btn btn-outline btn-sm" href="/vk-oauth/start">Получить VK user token</a></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-4 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>
<form method="post" action="/vk-poster-schedule/create" class="flex items-center gap-3 bg-base-200 p-3 rounded-xl"> <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">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="time" name="time" value="10:05" class="input input-bordered input-sm flex-1" required> <div class="flex-1 min-w-[120px]">
<input type="number" name="count" value="1" min="1" max="20" class="input input-bordered input-sm w-20" required> <label class="block text-[10px] uppercase font-bold text-app-textMuted mb-1">Время</label>
<label class="cursor-pointer label gap-2 p-0"> <input type="time" name="time" value="10:05" class="input input-sm w-full" required>
<input type="checkbox" name="enabled" class="checkbox checkbox-sm checkbox-primary" checked> </div>
</label> <div class="w-20">
<button class="btn btn-primary btn-sm" type="submit">Добавить</button> <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" required>
</div>
<div class="flex items-center h-8 px-2 border border-app-border rounded-lg bg-app-bg">
<label class="cursor-pointer flex items-center gap-2 group">
<input type="checkbox" name="enabled" class="checkbox w-4 h-4 rounded" checked>
<span class="text-xs font-medium text-app-textMain group-hover:text-white">Вкл</span>
</label>
</div>
<button class="btn btn-primary btn-sm h-8" type="submit">Добавить</button>
</form> </form>
<table class="table table-sm w-full"> <div class="overflow-x-auto border border-app-border rounded-xl">
<thead><tr><th>Время Екб</th><th>Постов</th><th>Статус</th><th></th></tr></thead> <table class="w-full text-left text-xs whitespace-nowrap bg-app-surface">
<tbody> <thead class="bg-app-bg border-b border-app-border text-app-textMuted uppercase tracking-wider text-[10px]">
{% for row in vk_schedule %} <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>
<tr class="hover"> </thead>
<td> <tbody class="divide-y divide-app-border">
<form id="vk-schedule-update-{{ row.id }}" method="post" action="/vk-poster-schedule/{{ row.id }}/update" class="m-0"> {% for row in vk_schedule %}
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <tr class="hover:bg-app-surfaceHover transition-colors">
</form> <td class="px-3 py-2">
<input form="vk-schedule-update-{{ row.id }}" type="time" name="time" value="{{ row.time }}" class="input input-bordered input-xs w-full" required> <form id="vk-schedule-update-{{ row.id }}" method="post" action="/vk-poster-schedule/{{ row.id }}/update" class="m-0">
</td> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<td><input form="vk-schedule-update-{{ row.id }}" type="number" name="count" value="{{ row.count }}" min="1" max="20" class="input input-bordered input-xs w-16" required></td> </form>
<td><label class="cursor-pointer"><input form="vk-schedule-update-{{ row.id }}" type="checkbox" name="enabled" class="toggle toggle-xs toggle-primary" {% if row.enabled %}checked{% endif %}></label></td> <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 class="flex gap-1"> </td>
<button form="vk-schedule-update-{{ row.id }}" type="submit" class="btn btn-ghost btn-xs btn-square"></button> <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>
<form method="post" action="/vk-poster-schedule/{{ row.id }}/delete" class="m-0"> <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>
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}"> <td class="px-3 py-2 flex gap-1 justify-center">
<button type="submit" class="btn btn-ghost btn-xs btn-square text-error"></button> <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> <form method="post" action="/vk-poster-schedule/{{ row.id }}/delete" class="m-0">
</td> <input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
</tr> <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>
{% endfor %} </form>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
</div> </details>
</div> </div>
</div> </div>
<div class="divider mb-8 text-2xl font-bold">Настройки</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"> <div class="flex flex-col gap-6">
{% for category, rows in settings|groupby("category") %} {% for category, rows in settings|groupby("category") %}
<div class="collapse collapse-arrow bg-base-100 shadow-sm border border-base-200" {% if category == "AI Qualifier" or loop.first %}open{% endif %}> <details class="card group/details" {% if category == "AI Qualifier" or loop.first %}open{% endif %}>
<input type="checkbox" {% if category == "AI Qualifier" or loop.first %}checked{% 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="collapse-title text-xl font-bold">{{ category_titles.get(category, category) }}</div> <div class="text-lg font-bold text-white">{{ category_titles.get(category, category) }}</div>
<div class="collapse-content border-t border-base-200 pt-4"> <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-6"> <div class="flex flex-col gap-8">
{% for s in rows %} {% for s in rows %}
<div class="flex flex-col md:flex-row gap-6 p-4 bg-base-200/30 rounded-xl"> <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 --> <!-- Description Sidebar -->
<div class="w-full md:w-1/3 flex flex-col gap-1"> <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-base-content">{{ s.key }}</div> <div class="font-mono font-bold text-sm text-app-primary">{{ s.key }}</div>
<div class="text-xs text-base-content/60">{{ s.title }}</div> <div class="text-sm font-semibold text-white mt-1">{{ s.title }}</div>
<div class="text-xs text-base-content/80 mt-2">{{ s.description }}</div> <div class="text-xs text-app-textMuted mt-2 leading-relaxed">{{ s.description }}</div>
</div> </div>
<!-- Control Panel --> <!-- Control Panel -->
<div class="flex-1"> <div class="flex-1">
<form method="post" action="/settings/save" class="flex flex-col items-start gap-3 w-full"> <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="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="key" value="{{ s.key }}"> <input type="hidden" name="key" value="{{ s.key }}">
<div class="flex gap-2 w-full"> <div class="flex gap-3 w-full items-start">
{% if s.value_type == "bool" %} <div class="flex-1">
<select name="value" class="select select-bordered select-sm w-32"> {% if s.value_type == "bool" %}
<option value="true" {% if s.value_json == true %}selected{% endif %}>true</option> <select name="value" class="select w-32">
<option value="false" {% if s.value_json == false %}selected{% endif %}>false</option> <option value="true" {% if s.value_json == true %}selected{% endif %}>true</option>
</select> <option value="false" {% if s.value_json == false %}selected{% endif %}>false</option>
</select>
{% elif s.key in ["ai_qualifier_provider", "ai_writer_provider"] %} {% elif s.key in ["ai_qualifier_provider", "ai_writer_provider"] %}
<select name="value" class="select select-bordered select-sm max-w-sm"> <select name="value" class="select max-w-sm w-full">
{% for p in provider_options %} {% for p in provider_options %}
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option> <option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% elif s.key == "ai_qualifier_model" %} {% elif s.key == "ai_qualifier_model" %}
<select name="value" class="select select-bordered select-sm max-w-sm"> <select name="value" class="select max-w-sm w-full">
{% for m in ai_model_options %} {% for m in ai_model_options %}
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option> <option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
{% endfor %} {% endfor %}
{% if s.value_json and s.value_json not in ai_model_options|map(attribute="value")|list %} {% 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> <option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
{% endif %} {% endif %}
</select> </select>
{% elif s.key == "ai_writer_model" %} {% elif s.key == "ai_writer_model" %}
<select name="value" class="select select-bordered select-sm max-w-sm"> <select name="value" class="select max-w-sm w-full">
{% for m in writer_model_options %} {% for m in writer_model_options %}
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option> <option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
{% endfor %} {% endfor %}
{% if s.value_json and s.value_json not in writer_model_options|map(attribute="value")|list %} {% 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> <option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
{% endif %} {% endif %}
</select> </select>
{% elif s.value_type == "secret" %} {% elif s.value_type == "secret" %}
<input type="password" name="value" value="{{ s.value_json }}" class="input input-bordered input-sm w-full max-w-md" autocomplete="off" spellcheck="false" placeholder="Секретный ключ"> <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" %} {% elif s.value_type == "text" %}
<div class="w-full flex flex-col gap-2"> <div class="w-full flex flex-col gap-3">
{% if prompt_hints and s.key in prompt_hints %} {% if prompt_hints and s.key in prompt_hints %}
<div class="bg-info/10 text-info-content text-xs p-4 rounded-lg flex flex-col gap-2"> <div class="bg-blue-900/20 border border-blue-500/30 text-blue-200 text-xs p-4 rounded-lg flex flex-col gap-2">
<strong>{{ prompt_hints[s.key].title }}</strong> <strong class="text-blue-400 font-bold flex items-center gap-2"><i data-lucide="info" class="w-4 h-4"></i> {{ prompt_hints[s.key].title }}</strong>
<div>{{ prompt_hints[s.key].body }}</div> <div class="leading-relaxed">{{ prompt_hints[s.key].body }}</div>
<div class="grid grid-cols-2 gap-4 mt-2"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-2 pt-2 border-t border-blue-500/20">
<div><span class="font-bold">Как безопасно менять:</span> {{ prompt_hints[s.key].safe }}</div> <div><span class="font-bold text-white uppercase tracking-wider text-[10px]">Как безопасно менять:</span><br>{{ prompt_hints[s.key].safe }}</div>
<div><span class="font-bold">На вход:</span> {{ prompt_hints[s.key].input }}</div> <div><span class="font-bold text-white uppercase tracking-wider text-[10px]">На вход:</span><br>{{ prompt_hints[s.key].input }}</div>
</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> </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 %} {% endif %}
<textarea name="value" class="textarea textarea-bordered font-mono text-xs w-full leading-relaxed" rows="12">{{ s.value_json }}</textarea>
</div> </div>
{% elif s.value_type == "json" %} <button class="btn btn-primary" type="submit">
<textarea name="value" class="textarea textarea-bordered font-mono text-xs w-full max-w-2xl leading-relaxed" rows="5" spellcheck="false">{{ s.value_json | tojson }}</textarea> <i data-lucide="save" class="w-4 h-4"></i> Сохранить
</button>
{% else %}
<input name="value" value="{{ s.value_json }}" class="input input-bordered input-sm max-w-xs">
{% endif %}
<button class="btn btn-primary btn-sm h-full" type="submit">OK</button>
</div> </div>
</form> </form>
</div> </div>
@@ -323,7 +392,7 @@
</div> </div>
</div> </div>
</div> </details>
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}