diff --git a/src/vk_parser_app/admin.py b/src/vk_parser_app/admin.py
index 39f3969..d24e179 100644
--- a/src/vk_parser_app/admin.py
+++ b/src/vk_parser_app/admin.py
@@ -2020,8 +2020,9 @@ async def sources_list(request: Request, q: str = "", status_filter: str = "", p
item["last_checked_at_fmt"] = format_dt(item.get("last_checked_at"))
item["created_at_fmt"] = format_dt(item.get("created_at"))
sources.append(item)
+ template_name = "sources_content.html" if request.headers.get("hx-request") else "sources.html"
return templates.TemplateResponse(
- "sources.html",
+ template_name,
base_context(
request,
user,
@@ -2317,6 +2318,25 @@ async def source_toggle(request: Request, source_id: int, csrf_token: str = Form
source_id,
)
await audit(user["id"], "source.toggle", "source", source_id, {"active": bool(row["active"]) if row else None})
+ if request.headers.get("hx-request"):
+ # Fetch the updated source
+ source_row = await pool.fetchrow("SELECT * FROM sources WHERE id=$1", source_id)
+ if source_row:
+ s = dict(source_row)
+ # Fetch stats just for this source to render correctly
+ stats = await pool.fetchrow(
+ """
+ SELECT COUNT(*) AS total_posts,
+ COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '24 hours') AS recent_posts
+ FROM raw_posts
+ WHERE source_id=$1
+ """,
+ source_id
+ )
+ s["posts_count"] = stats["total_posts"]
+ s["posts_24h"] = stats["recent_posts"]
+ s["last_parsed_at_fmt"] = s["last_parsed_at"].strftime("%d.%m.%Y %H:%M") if s.get("last_parsed_at") else None
+ return templates.TemplateResponse("source_row.html", base_context(request, user, s=s))
return redirect("/sources")
@@ -2329,11 +2349,12 @@ async def source_delete(request: Request, source_id: int, csrf_token: str = Form
pool = await get_pool()
await pool.execute("UPDATE sources SET archived_at=NOW(), active=FALSE, updated_at=NOW() WHERE id=$1", source_id)
await audit(user["id"], "source.archive", "source", source_id)
+ if request.headers.get("hx-request"):
+ return HTMLResponse("")
return redirect("/sources")
@app.get("/raw", response_class=HTMLResponse)
-@app.get("/v2/raw", response_class=HTMLResponse)
async def raw_posts(
request: Request,
status_filter: str = "",
@@ -2482,11 +2503,7 @@ async def raw_posts(
)
posts = [prepare_raw_post(row) for row in rows]
- is_v2 = request.url.path.startswith("/v2")
- if is_v2:
- template_name = "v2/raw_posts_content.html" if request.headers.get("hx-request") else "v2/raw_posts.html"
- else:
- template_name = "raw_posts.html"
+ template_name = "raw_posts_content.html" if request.headers.get("hx-request") else "raw_posts.html"
return templates.TemplateResponse(
template_name,
@@ -2567,6 +2584,58 @@ async def raw_send_to_rewrite(
return redirect(safe_next)
+async def fetch_single_editor_row(pool, post_id: int):
+ return await pool.fetchrow(
+ """
+ SELECT rp.*, s.name AS source_name, s.tag AS source_tag, s.platform AS source_platform, s.url AS source_url,
+ u.login AS reviewed_by_login,
+ COALESCE(c_id.tag, c_name.tag) AS resolved_category_tag,
+ COUNT(rpm.id) AS media_count,
+ COALESCE(
+ jsonb_agg(
+ jsonb_build_object(
+ 'id', rpm.id,
+ 'type', rpm.media_type,
+ 'url', rpm.original_url,
+ 'status', rpm.status,
+ 'error', rpm.error,
+ 'duration_sec', rpm.duration_sec
+ )
+ ORDER BY rpm.sort_order ASC, rpm.id ASC
+ ) FILTER (WHERE rpm.id IS NOT NULL),
+ '[]'::jsonb
+ ) AS media_items,
+ COALESCE(
+ (
+ SELECT jsonb_agg(
+ jsonb_build_object(
+ 'platform', pp.platform,
+ 'status', pp.status,
+ 'target_id', pp.target_id,
+ 'external_id', pp.external_id,
+ 'url', pp.url,
+ 'error', pp.error,
+ 'published_at', pp.published_at
+ )
+ ORDER BY pp.platform
+ )
+ FROM post_publications pp
+ WHERE pp.raw_post_id=rp.id
+ ),
+ '[]'::jsonb
+ ) AS platform_publications
+ FROM raw_posts rp
+ JOIN sources s ON s.id=rp.source_id
+ LEFT JOIN admin_users u ON u.id=rp.reviewed_by
+ LEFT JOIN content_categories c_id ON c_id.sort_order=COALESCE(rp.final_category_id, rp.rewrite_category_id)
+ LEFT JOIN content_categories c_name ON lower(c_name.name)=lower(COALESCE(rp.final_category, rp.rewrite_category, ''))
+ LEFT JOIN raw_post_media rpm ON rpm.raw_post_id=rp.id AND COALESCE(rpm.editor_hidden, FALSE)=FALSE
+ WHERE rp.id = $1
+ GROUP BY rp.id, s.name, s.tag, s.platform, s.url, u.login, c_id.tag, c_name.tag
+ """,
+ post_id
+ )
+
@app.get("/editor", response_class=HTMLResponse)
async def editor_feed(
request: Request,
@@ -2709,8 +2778,9 @@ async def editor_feed(
"""
)
counts = {str(row["status"]): int(row["count"]) for row in counts_rows}
+ template_name = "editor_content.html" if request.headers.get("hx-request") else "editor.html"
return templates.TemplateResponse(
- "editor.html",
+ template_name,
base_context(
request,
user,
@@ -2782,6 +2852,14 @@ async def editor_accept(request: Request, post_id: int, csrf_token: str = Form(.
user["id"],
)
await audit(user["id"], "editor.accept", "raw_post", post_id)
+ if request.headers.get("hx-request"):
+ row = await fetch_single_editor_row(pool, post_id)
+ if row:
+ categories = await writer_category_options()
+ return templates.TemplateResponse(
+ "editor_post.html",
+ base_context(request, user, p=prepare_editor_post(row), categories=categories)
+ )
return redirect("/editor")
@@ -2807,6 +2885,14 @@ async def editor_reject(request: Request, post_id: int, csrf_token: str = Form(.
user["id"],
)
await audit(user["id"], "editor.reject", "raw_post", post_id)
+ if request.headers.get("hx-request"):
+ row = await fetch_single_editor_row(pool, post_id)
+ if row:
+ categories = await writer_category_options()
+ return templates.TemplateResponse(
+ "editor_post.html",
+ base_context(request, user, p=prepare_editor_post(row), categories=categories)
+ )
return redirect("/editor")
@@ -2934,6 +3020,14 @@ async def editor_save(
user["id"],
)
await audit(user["id"], "editor.save" if status_value == "edited" else "editor.save_accept", "raw_post", post_id)
+ if request.headers.get("hx-request"):
+ row = await fetch_single_editor_row(pool, post_id)
+ if row:
+ categories = await writer_category_options()
+ return templates.TemplateResponse(
+ "editor_post.html",
+ base_context(request, user, p=prepare_editor_post(row), categories=categories)
+ )
return redirect("/editor")
diff --git a/src/vk_parser_app/templates/base.html b/src/vk_parser_app/templates/base.html
index 5e5a6f0..d125d06 100644
--- a/src/vk_parser_app/templates/base.html
+++ b/src/vk_parser_app/templates/base.html
@@ -1,246 +1,65 @@
-
+
{{ title or "VK Parser Admin" }}
+
+
+
+
+
+
+
+
+
+
-
-{% if user %}
-
-{% endif %}
-{% block body %}{% endblock %}
+
+
+ {% if user %}
+
+
+ Parser Admin
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+ {% block body %}{% endblock %}
+
+
+
diff --git a/src/vk_parser_app/templates/editor.html b/src/vk_parser_app/templates/editor.html
index 31d31c9..f41735f 100644
--- a/src/vk_parser_app/templates/editor.html
+++ b/src/vk_parser_app/templates/editor.html
@@ -1,359 +1,24 @@
{% extends "base.html" %}
{% block body %}
-
+
-
Редакторская
-
Посты после AI-рерайта: быстрая проверка, правка и принятие.
+
Редакторская
+
Посты после AI-рерайта: быстрая проверка, правка и принятие.
-
-
-
-
-
-
-
-
+
+
{% endblock %}
diff --git a/src/vk_parser_app/templates/editor_content.html b/src/vk_parser_app/templates/editor_content.html
new file mode 100644
index 0000000..ca2d37e
--- /dev/null
+++ b/src/vk_parser_app/templates/editor_content.html
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+ Всего постов: {{ pagination.total }}
+
+
+ На странице:
+
+
+
+
+
+ {% for p in posts %}
+ {% include "editor_post.html" %}
+ {% else %}
+
+ В этой очереди пока нет постов.
+
+ {% endfor %}
+
+
+
+ {% if pagination.total_pages > 1 %}
+
+
+
+ {% endif %}
+
+
diff --git a/src/vk_parser_app/templates/editor_post.html b/src/vk_parser_app/templates/editor_post.html
new file mode 100644
index 0000000..c0dde4c
--- /dev/null
+++ b/src/vk_parser_app/templates/editor_post.html
@@ -0,0 +1,165 @@
+
+
+
+
+
+ {% for m in p.media_items %}
+ {% if m.type == "photo" and m.url %}
+
+

+
+ {% elif m.url %}
+
▶
+ {% endif %}
+ {% endfor %}
+
+
{{ p.media_count or 0 }} медиа
+
+
+
+
+
+
+
{{ p.source_name }}
+
•
+
#{{ p.review_source_tag or p.source_tag or "source" }}
+
•
+
{{ p.posted_at_fmt or p.created_at_fmt }}
+
+
+
+ {{ p.editorial_status_label }}
+
+ {{ p.review_category or "Без категории" }}
+ AI {{ p.qualification_score or "?" }}/10
+
+
+
+
+ {% for pub in p.publication_platforms %}
+ {% if pub.url %}
+
+
+ {{ pub.label }}
+
+ {% else %}
+
+ {{ pub.label }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {{ p.publication_preview_text }}
+
+
+
+
+
Исходник и AI-заметки
+
+
+
Оригинал
+
{{ p.raw_text }}
+
+
+
Заметки
+
{{ p.rewrite_notes or p.qualification_reason or "—" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/vk_parser_app/templates/login.html b/src/vk_parser_app/templates/login.html
index 9dd4d56..25ad63f 100644
--- a/src/vk_parser_app/templates/login.html
+++ b/src/vk_parser_app/templates/login.html
@@ -1,12 +1,30 @@
{% extends "base.html" %}
{% block body %}
-
-
Вход
- {% if error %}
{{ error }}
{% endif %}
-
-
-
-
-
+
+
+
+
Вход в систему
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
diff --git a/src/vk_parser_app/templates/logs.html b/src/vk_parser_app/templates/logs.html
index 8b9b001..f65326c 100644
--- a/src/vk_parser_app/templates/logs.html
+++ b/src/vk_parser_app/templates/logs.html
@@ -1,105 +1,142 @@
{% extends "base.html" %}
{% block body %}
-
-
-
Логи
-
Журнал действий и запросов админки. События старше 30 дней удаляются автоматически.
-
+
+
Логи
+
Журнал действий и запросов админки. События старше 30 дней удаляются автоматически.
-
-
-
-