feat: complete UI redesign of admin dashboard with custom Tailwind
This commit is contained in:
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
<!-- Content Preview -->
|
||||||
<a href="{{ p.original_url }}" target="_blank" class="link link-hover font-medium text-base-content">{{ p.source_name }}</a>
|
<div class="p-5 flex-1 text-sm text-app-textMain leading-relaxed whitespace-pre-wrap font-sans">
|
||||||
<span>•</span>
|
|
||||||
<span class="badge badge-sm">#{{ p.review_source_tag or p.source_tag or "source" }}</span>
|
|
||||||
<span>•</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">
|
|
||||||
{% for pub in p.publication_platforms %}
|
|
||||||
{% 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> • {{ 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>
|
||||||
|
• {{ 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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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-6">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<h2 class="text-sm font-bold text-app-textMuted uppercase tracking-wider">Фильтры</h2>
|
||||||
<h2 class="card-title text-lg">Фильтры</h2>
|
<a href="/raw" class="text-xs text-app-primary hover:text-app-primaryHover transition-colors" hx-boost="true" hx-target="#content-area">Сбросить</a>
|
||||||
<a href="/raw" class="text-sm link link-hover text-base-content/60" 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 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 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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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"] %}
|
|
||||||
<select name="value" class="select select-bordered select-sm max-w-sm">
|
{% elif s.key in ["ai_qualifier_provider", "ai_writer_provider"] %}
|
||||||
{% for p in provider_options %}
|
<select name="value" class="select max-w-sm w-full">
|
||||||
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
|
{% for p in provider_options %}
|
||||||
{% endfor %}
|
<option value="{{ p.value }}" {% if s.value_json == p.value %}selected{% endif %}>{{ p.label }}</option>
|
||||||
</select>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
{% elif s.key == "ai_qualifier_model" %}
|
|
||||||
<select name="value" class="select select-bordered select-sm max-w-sm">
|
{% elif s.key == "ai_qualifier_model" %}
|
||||||
{% for m in ai_model_options %}
|
<select name="value" class="select max-w-sm w-full">
|
||||||
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
|
{% for m in ai_model_options %}
|
||||||
{% endfor %}
|
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
|
||||||
{% if s.value_json and s.value_json not in ai_model_options|map(attribute="value")|list %}
|
{% endfor %}
|
||||||
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
|
{% if s.value_json and s.value_json not in ai_model_options|map(attribute="value")|list %}
|
||||||
{% endif %}
|
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
|
||||||
</select>
|
{% endif %}
|
||||||
|
</select>
|
||||||
{% elif s.key == "ai_writer_model" %}
|
|
||||||
<select name="value" class="select select-bordered select-sm max-w-sm">
|
{% elif s.key == "ai_writer_model" %}
|
||||||
{% for m in writer_model_options %}
|
<select name="value" class="select max-w-sm w-full">
|
||||||
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
|
{% for m in writer_model_options %}
|
||||||
{% endfor %}
|
<option value="{{ m.value }}" {% if s.value_json == m.value %}selected{% endif %}>{{ m.label }}</option>
|
||||||
{% if s.value_json and s.value_json not in writer_model_options|map(attribute="value")|list %}
|
{% endfor %}
|
||||||
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
|
{% if s.value_json and s.value_json not in writer_model_options|map(attribute="value")|list %}
|
||||||
{% endif %}
|
<option value="{{ s.value_json }}">{{ s.value_json }} · текущее значение</option>
|
||||||
</select>
|
{% endif %}
|
||||||
|
</select>
|
||||||
{% 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="Секретный ключ">
|
{% elif s.value_type == "secret" %}
|
||||||
|
<input type="password" name="value" value="{{ s.value_json }}" class="input w-full max-w-md" autocomplete="off" spellcheck="false" placeholder="Секретный ключ">
|
||||||
{% elif s.value_type == "text" %}
|
|
||||||
<div class="w-full flex flex-col gap-2">
|
{% elif s.value_type == "text" %}
|
||||||
{% if prompt_hints and s.key in prompt_hints %}
|
<div class="w-full flex flex-col gap-3">
|
||||||
<div class="bg-info/10 text-info-content text-xs p-4 rounded-lg flex flex-col gap-2">
|
{% if prompt_hints and s.key in prompt_hints %}
|
||||||
<strong>{{ prompt_hints[s.key].title }}</strong>
|
<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">
|
||||||
<div>{{ prompt_hints[s.key].body }}</div>
|
<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 class="grid grid-cols-2 gap-4 mt-2">
|
<div class="leading-relaxed">{{ prompt_hints[s.key].body }}</div>
|
||||||
<div><span class="font-bold">Как безопасно менять:</span> {{ prompt_hints[s.key].safe }}</div>
|
<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].input }}</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 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" %}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{% 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>
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i data-lucide="save" class="w-4 h-4"></i> Сохранить
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +392,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user