feat(zero): admin UI for Zero notes management

Adds new admin section 'Заметки Зеро' () with:
  - manual 'Generate' button with channel/bucket/allow-dup form
  - status filter tabs with counters (draft/approved/published/failed/skipped)
  - per-note actions: approve / edit inline / regenerate with bucket pick / skip
  - status-colored cards with bucket icon, pose, scheduled time MSK
  - error display with attempt counter
  - tokens & model footer

Files:
  app/api/admin/zero/[...path]/route.js  catch-all proxy → engine
  components/admin/AdminZero.js          main component
  components/AdminPanel.js               +section in sidebar
This commit is contained in:
Aleksei Pavlov
2026-06-19 10:53:00 +03:00
parent 68fb51fc0a
commit 5bc413da50
3 changed files with 446 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
/**
* Catch-all proxy для /api/admin/zero/* → engine /api/admin/zero/*
* Принимает любой метод и любой путь. Auth: session cookie → user.isAdmin.
*/
import { NextResponse } from 'next/server';
import { requireUser } from '@/lib/session';
const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
const ENGINE_SECRET = process.env.ENGINE_SECRET || '';
async function proxy(req, { params }) {
const user = await requireUser();
if (!user?.isAdmin) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const tail = (params?.path || []).join('/');
const qs = req.url.split('?')[1];
const url = `${ENGINE_URL}/api/admin/zero${tail ? '/' + tail : ''}${qs ? '?' + qs : ''}`;
const headers = {
'x-internal-secret': ENGINE_SECRET,
'x-user-id': String(user.id),
};
let body;
if (req.method !== 'GET' && req.method !== 'HEAD') {
const ct = req.headers.get('content-type') || '';
if (ct.includes('application/json')) {
headers['Content-Type'] = 'application/json';
const raw = await req.text();
body = raw || undefined;
} else {
body = await req.text();
}
}
const res = await fetch(url, { method: req.method, headers, body, cache: 'no-store' });
const data = await res.json().catch(() => ({ error: 'invalid engine response' }));
return NextResponse.json(data, { status: res.status });
}
export const GET = proxy;
export const POST = proxy;
export const PATCH = proxy;
export const PUT = proxy;
export const DELETE = proxy;