Найден корректный robots.txt
Файл управления сканированием сайта роботами
?
Robots.txt указывает поисковым роботам, какие страницы сканировать, а какие — нет. Ошибки в файле могут случайно закрыть важные разделы от индексации.
Robots.txt настроен корректно. Размер файла: 63917 байт. Загружен за: 97мсек.
Проверяемая страница не запрещена в robots.txt.
Показать содержимое robots.txt
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>МногоПост - автопостинг</title>
<style>
:root{--bg:#f3f1ea;--ink:#171717;--muted:#6f6b62;--panel:#fffdf8;--line:rgba(23,23,23,.12);--dark:#141614;--green:#73e35f;--coral:#ff6f61;--cobalt:#286df0}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Segoe UI,Arial,sans-serif}button,input,textarea,select{font:inherit}button{cursor:pointer}.hidden{display:none!important}
.login-page{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(115deg,rgba(20,22,20,.94),rgba(20,22,20,.68)),url("https://mnogopost-autoposting.inteh.chatgpt.site/og.png") center/cover}.login-card{position:relative;width:min(100%,980px);display:grid;grid-template-columns:1.05fr .95fr;overflow:hidden;border:1px solid rgba(255,255,255,.18);border-radius:28px;background:rgba(255,253,248,.94);box-shadow:0 40px 120px rgba(0,0,0,.35)}.login-close{position:absolute;top:18px;right:18px;z-index:5;width:42px;height:42px;border:1px solid var(--line);border-radius:999px;background:white;font-size:28px;font-weight:900}.login-visual{min-height:520px;display:flex;flex-direction:column;justify-content:flex-end;padding:34px;color:white;background:linear-gradient(180deg,rgba(20,22,20,.08),rgba(20,22,20,.94)),url("https://mnogopost-autoposting.inteh.chatgpt.site/og.png") center/cover}.brand{display:flex;align-items:center;gap:12px;color:inherit;text-decoration:none;font-size:20px;font-weight:900}.mark,.avatar{display:grid;place-items:center;width:42px;height:42px;border-radius:12px;background:var(--green);color:#101410;font-weight:900}.login-visual h1{font-size:clamp(38px,5vw,62px);line-height:.98}.login-form{display:grid;align-content:center;gap:16px;padding:42px}
input,textarea,select{width:100%;border:1px solid var(--line);border-radius:14px;padding:13px 14px;background:#fbfaf6}label{display:grid;gap:8px;font-size:13px;font-weight:900}.action{min-height:46px;border:0;border-radius:14px;padding:12px 16px;background:var(--green);font-weight:900}.secondary{min-height:42px;border:1px solid var(--line);border-radius:13px;padding:10px 13px;background:#fbfaf6;font-weight:900}
.admin{min-height:100vh;display:grid;grid-template-columns:280px 1fr}.side{position:sticky;top:0;height:100vh;padding:24px;display:flex;flex-direction:column;gap:24px;color:white;background:#141614}.side nav{display:grid;gap:8px}.side button{border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:13px;color:rgba(255,255,255,.72);text-align:left;background:rgba(255,255,255,.04);font-weight:800}.side button.active{background:var(--green);color:#101410}.main{width:min(100%,1320px);margin:0 auto;padding:28px}.hero,.panel{border:1px solid var(--line);border-radius:22px;background:rgba(255,253,248,.9);box-shadow:0 24px 70px rgba(39,35,25,.08)}.hero{min-height:240px;display:grid;grid-template-columns:1fr 220px;align-items:end;gap:24px;padding:34px;color:white;background:linear-gradient(115deg,rgba(20,22,20,.94),rgba(20,22,20,.72)),url("https://mnogopost-autoposting.inteh.chatgpt.site/og.png") center/cover}.hero h1{font-size:clamp(40px,5vw,68px);line-height:.96}.panel{padding:22px;margin-top:18px}.grid{display:grid;grid-template-columns:1.2fr .8fr;gap:18px}.bottom{display:grid;grid-template-columns:.8fr 1.2fr;gap:18px}.title{display:flex;justify-content:space-between;gap:18px;align-items:center;margin-bottom:18px}.eyebrow{color:#2e7c25;font-size:12px;font-weight:900;text-transform:uppercase}.row{display:grid;grid-template-columns:1fr 180px auto;gap:12px;align-items:end}.pick{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.channel,.card,.post,.log{border:1px solid var(--line);border-radius:16px;padding:16px;background:#fbfaf6}.channel{display:flex;gap:12px;text-align:left}.icon{flex:0 0 46px;width:46px;height:46px;display:grid;place-items:center;border-radius:14px;color:white;font-weight:1000}.selected{box-shadow:inset 0 0 0 2px rgba(115,227,95,.7)}.blue{background:#2877f0}.cyan{background:#1fa9e8}.rose{background:linear-gradient(135deg,#f8c33a,#ee4c64,#7e43d6)}.orange{background:#f28722}.ink{background:#1c1f23}.red{background:#eb342f}.green{background:#27a567}.violet{background:#6f52ff}.pill,.chip{display:inline-block;border-radius:999px;padding:8px 10px;background:#eff8ed;color:#2e7c25;font-size:12px;font-weight:900;margin:4px}.danger{background:#fff0ed;color:#a23226}.list{display:grid;gap:12px}.post{display:grid;grid-template-columns:1fr 190px;gap:18px}.actions{display:grid;align-content:start;gap:8px}.notice{margin-top:18px;border-radius:16px;padding:14px;background:#eff8ed;color:#235f1d;font-weight:800}textarea{min-height:220px;resize:vertical}.preview{white-space:pre-wrap}.muted{color:var(--muted);line-height:1.5}@media(max-width:900px){.admin,.login-card,.grid,.bottom,.hero,.post,.row{grid-template-columns:1fr}.side{position:static;height:auto}.pick{grid-template-columns:1fr}}
.metric{display:grid;gap:6px;border:1px solid var(--line);border-radius:16px;padding:16px;background:#fbfaf6}.metric b{font-size:32px}.ops{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.steps{counter-reset:step}.steps .card{position:relative;padding-left:52px}.steps .card:before{counter-increment:step;content:counter(step);position:absolute;left:16px;top:16px;width:24px;height:24px;display:grid;place-items:center;border-radius:999px;background:var(--green);font-weight:900}.copybox{min-height:230px;font-family:Consolas,monospace;font-size:13px}.warn{background:#fff4db;color:#765100}.ok{background:#eff8ed;color:#235f1d}@media(max-width:1100px){.ops{grid-template-columns:repeat(2,1fr)}}@media(max-width:700px){.ops{grid-template-columns:1fr}}
</style>
</head>
<body>
<main id="login" class="login-page"><section class="login-card"><button class="login-close" onclick="enter()">×</button><div class="login-visual"><a class="brand"><span class="mark">М</span><span>МногоПост</span></a><h1>Вход в админку автопостинга</h1><p>Рабочая версия на SpaceWeb: редактор, очередь, каналы, RSS-источники и журнал.</p></div><div class="login-form"><p class="eyebrow">Тестовый доступ</p><h2>Авторизация</h2><label>Логин<input id="loginInput"></label><label>Пароль<input id="passwordInput" type="password" onkeydown="if(event.key==='Enter') signIn()"></label><button class="action" onclick="signIn()">Войти</button><p class="muted">логин: тест / пароль: тест</p><p id="loginError" class="danger"></p></div></section></main>
<main id="app" class="admin hidden"><aside class="side"><a class="brand"><span class="mark">М</span><span>МногоПост</span></a><nav id="nav"></nav><div style="margin-top:auto" class="muted">Пользователь<br><b id="userLabel"></b><br><br><button onclick="logout()">Выйти</button></div></aside><section class="main"><header class="hero"><div><p class="eyebrow">SpaceWeb версия</p><h1>Генерация, очередь, источники и автопостинг</h1><p>Сайт размещен на `mnogopost.ru`; данные этой версии хранятся на SpaceWeb в JSON.</p></div><div><b style="font-size:54px" id="postCount">0</b><br>постов</div></header><div id="notice" class="notice hidden"></div><section id="view"></section></section></main>
<script>
let user=null, channels=[], connections=[], posts=[], logs=[], sources=[], news=[], settings=null, tab='Создать пост', selected=['vk','telegram','dzen'], mode='auto';
const tabs=['Публикации','Создать пост','Источники','Каналы','Настройки','Журнал'];
const labels={draft:'черновик',pending_approval:'на согласовании',approved:'согласовано',scheduled:'в очереди',publishing:'публикуется',published:'опубликовано',failed:'ошибка',manual_required:'вручную',connected:'подключено',not_connected:'не подключено',requires_oauth:'нужен OAuth',manual:'ручной режим'};
async function api(url, opts={}){const r=await fetch(url,{...opts,headers:{'Content-Type':'application/json',...(opts.headers||{})}});const d=await r.json().catch(()=>({}));if(!r.ok)throw Error(d.error||'Запрос не выполнен');return d}
function enter(){user={login:'тест',role:'owner'};showApp()}
async function signIn(){try{const d=await api('/api/login',{method:'POST',body:JSON.stringify({login:loginInput.value,password:passwordInput.value})});user=d.user;showApp()}catch(e){loginError.textContent=e.message}}
function logout(){user=null;login.classList.remove('hidden');app.classList.add('hidden')}
async function showApp(){login.classList.add('hidden');app.classList.remove('hidden');userLabel.textContent=user.login+' / '+user.role;await refresh()}
async function refresh(){const [c,p,l,s,n,set]=await Promise.all([api('/api/channels'),api('/api/posts'),api('/api/logs'),api('/api/news/sources'),api('/api/news/items'),api('/api/settings')]);channels=c.channels;connections=c.connections;posts=p.posts;logs=l.logs;sources=s.sources;news=n.items;settings=set;postCount.textContent=posts.length;renderNav();render()}
function note(t){notice.textContent=t;notice.classList.remove('hidden')}
function renderNav(){nav.innerHTML=tabs.map(t=>`<button class="${t===tab?'active':''}" onclick="tab='${t}';renderNav();render()">${t}</button>`).join('')}
function render(){if(tab==='Создать пост')return renderCreate();if(tab==='Публикации')return renderPosts();if(tab==='Источники')return renderSources();if(tab==='Каналы')return renderChannels();if(tab==='Настройки')return renderSettings();renderLogs()}
function renderCreate(){view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Пост</p><h2>Редактор публикации</h2></div><select id="mode" onchange="mode=this.value"><option value="auto">Автогенерация</option><option value="manual">Вручную</option></select></div><div class="row"><label>Тема новости<input id="topic" value="Запуск сервиса автоматического постинга новостей"></label><label>Тон<select id="tone"><option>деловой</option><option>живой</option><option>экспертный</option></select></label><button class="action" onclick="generate()">Сгенерировать</button></div><label>Заголовок<input id="title" value="Запуск МногоПост"></label><label>Текст<textarea id="text">Сервис готовит публикации для нескольких соцсетей, адаптирует текст под лимиты каналов и ставит посты в очередь.</textarea></label><div class="row"><label>Ссылка<input id="link" placeholder="https://..."></label><label>Media URL<input id="media" placeholder="https://.../image.jpg"></label><label>Дата<input id="date" type="datetime-local"></label></div><p><button class="secondary" onclick="save()">Сохранить черновик</button> <button class="secondary" onclick="save('pending_approval')">На согласование</button> <button class="action" onclick="save('scheduled')">Добавить в очередь</button></p></div><div class="panel"><p class="eyebrow">Соцсети</p><h2>Куда публикуем</h2><div class="pick">${channels.map(c=>`<button class="channel ${selected.includes(c.id)?'selected':''}" onclick="toggle('${c.id}')"><span class="icon ${c.tone}">${c.short}</span><span><b>${c.name}</b><br><small>${c.feature}</small></span></button>`).join('')}</div></div></section>`}
function toggle(id){selected=selected.includes(id)?selected.filter(x=>x!==id):[...selected,id];if(!selected.length)selected=['telegram'];renderCreate()}
async function generate(){const d=await api('/api/generate-post',{method:'POST',body:JSON.stringify({topic:topic.value,tone:tone.value,channels:selected,link:link.value})});title.value=d.generated.title;text.value=d.generated.main_text;note('Черновик сгенерирован.')}
async function save(status){const d=await api('/api/posts',{method:'POST',body:JSON.stringify({title:title.value,text:text.value,mode,channels:selected,scheduled_at:date.value?new Date(date.value).toISOString():null,media_urls:media.value?[media.value]:[]})});if(status==='scheduled')await api(`/api/posts/${d.post.id}/schedule`,{method:'POST',body:JSON.stringify({scheduled_at:date.value?new Date(date.value).toISOString():new Date().toISOString()})});if(status==='pending_approval')await api(`/api/posts/${d.post.id}/approve`,{method:'POST',body:'{}'});await refresh();tab='Публикации';renderNav();render();note(status==='scheduled'?'Пост добавлен в очередь.':'Черновик сохранен.')}
function renderPosts(){view.innerHTML=`<section class="panel"><div class="title"><div><p class="eyebrow">Посты</p><h2>Очередь и статусы</h2></div><button class="action" onclick="publishDue()">Запустить планировщик</button></div><div class="list">${posts.length?posts.map(p=>`<article class="post"><div><time>${p.scheduled_at?new Date(p.scheduled_at).toLocaleString('ru-RU'):'без даты'}</time><h3>${p.title}</h3><p class="preview">${p.text}</p>${p.channels.map(c=>`<span class="chip">${c.channel}: ${labels[c.status]||c.status}</span>`).join('')}</div><div class="actions"><span class="pill">${labels[p.status]||p.status}</span><button class="secondary" onclick="act('${p.id}','approve')">Согласовать</button><button class="secondary" onclick="act('${p.id}','schedule')">В очередь</button><button class="secondary" onclick="act('${p.id}','publish-now')">Опубликовать</button></div></article>`).join(''):'<p class="muted">Пока нет постов.</p>'}</div></section>`}
async function act(id,a){await api(`/api/posts/${id}/${a}`,{method:'POST',body:'{}'});await refresh();note('Действие выполнено.')}
async function publishDue(){const d=await api('/api/publish-due',{method:'POST',body:'{}'});await refresh();note('Планировщик обработал публикаций: '+d.count)}
function renderSources(){view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">RSS</p><h2>Источники новостей</h2></div><button class="action" onclick="importNews()">Импортировать</button></div><div class="row"><label>Название<input id="sourceName" value="Новости"></label><label>RSS URL<input id="sourceUrl" placeholder="https://site.ru/rss.xml"></label><button class="secondary" onclick="addSource()">Добавить</button></div><div class="list">${sources.map(s=>`<div class="card"><b>${s.name}</b><p class="muted">${s.url}</p><small>${s.status}</small></div>`).join('')||'<p class="muted">Пока нет источников.</p>'}</div></div><div class="panel"><p class="eyebrow">Импорт</p><h2>Найденные новости</h2><div class="list">${news.map(n=>`<div class="card"><b>${n.title}</b><p class="muted">${n.content_excerpt||n.url}</p><button class="secondary" onclick='draftFromNews(${JSON.stringify(n)})'>В редактор</button></div>`).join('')||'<p class="muted">После импорта здесь появятся новости.</p>'}</div></div></section>`}
async function addSource(){await api('/api/news/sources',{method:'POST',body:JSON.stringify({name:sourceName.value,url:sourceUrl.value})});await refresh();note('RSS-источник добавлен.')}
async function importNews(){const d=await api('/api/news/import',{method:'POST',body:'{}'});await refresh();note('Импортировано новостей: '+d.count)}
function draftFromNews(n){tab='Создать пост';renderNav();render();setTimeout(()=>{topic.value=n.title;title.value=n.title;text.value=(n.content_excerpt||'')+'\\n\\nИсточник: '+n.url;link.value=n.url},0)}
function renderChannels(){view.innerHTML=`<section class="panel"><p class="eyebrow">Интеграции</p><h2>Каналы и API-готовность</h2><div class="pick">${channels.map(c=>{let cn=connections.find(x=>x.channel===c.id)||{};return `<article class="card"><span class="icon ${c.tone}">${c.short}</span><h3>${c.name}</h3><p class="muted">${cn.runtime_message||c.feature}</p><b>${labels[cn.runtime_status||cn.status]||'не подключено'}</b></article>`}).join('')}</div></section>`}
function renderSettings(){view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Telegram</p><h2>Бот и права канала</h2><p class="muted">Токен хранится в JSON вне public_html. В чат он не выводится, видна только маска.</p><label>Bot token<input id="tgToken" placeholder="${settings?.telegram?.bot_token_masked||'123456:ABC...'}"></label><label>Chat ID или @channel<input id="tgChat" placeholder="${settings?.telegram?.chat_id_masked||'@channel или -100...'}"></label><p><button class="action" onclick="saveTelegram()">Сохранить Telegram</button> <button class="secondary" onclick="checkTelegram()">Проверить права</button></p><p class="muted">Статус: ${settings?.telegram?.configured?'настроен':'не настроен'}</p></div><div class="panel"><p class="eyebrow">Автозапуск</p><h2>Cron URL</h2><p class="muted">Поставьте этот URL в планировщик SpaceWeb или внешний cron: раз в 5 минут. Он обрабатывает отложенные публикации.</p><input readonly value="${settings?.cron?.url||''}" onclick="this.select()"><p class="muted">Последний запуск: ${settings?.cron?.last_cron_at||'еще не запускался'}<br>PHP: ${settings?.runtime?.php_version||''}<br>Хранилище: ${settings?.runtime?.store_path||''}</p><button class="secondary" onclick="runCron()">Запустить cron сейчас</button></div></section>`}
async function saveTelegram(){await api('/api/settings/telegram',{method:'POST',body:JSON.stringify({bot_token:tgToken.value,chat_id:tgChat.value})});await refresh();note('Telegram-доступы сохранены. Теперь проверьте права.')}
async function checkTelegram(){try{const d=await api('/api/telegram/check',{method:'POST',body:'{}'});await refresh();note(d.message||'Проверка выполнена.')}catch(e){note('Ошибка проверки Telegram: '+e.message)}}
async function runCron(){const d=await api('/api/cron/run',{method:'POST',body:'{}'});await refresh();note('Cron выполнен. Обработано публикаций: '+d.published)}
function renderLogs(){view.innerHTML=`<section class="panel"><p class="eyebrow">Логи</p><h2>Журнал публикаций</h2><div class="list">${logs.map(l=>`<article class="log"><time>${new Date(l.created_at).toLocaleString('ru-RU')}</time> <b>${l.channel}</b> <span class="pill ${l.status==='error'?'danger':''}">${l.action} / ${l.status}</span><p>${l.message}</p></article>`).join('')||'<p class="muted">Пока нет событий.</p>'}</div></section>`}
</script>
<script>
var tgSelected=[], currentNews=null;
var esc=function(s){return String(s??'').replace(/[&<>"']/g,function(m){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]})};
var oldRefresh=refresh;
refresh=async function(){await oldRefresh();if(!tgSelected.length&&settings?.telegram?.channels){tgSelected=settings.telegram.channels.filter(x=>x.enabled&&x.is_default).map(x=>x.id);if(!tgSelected.length)tgSelected=settings.telegram.channels.filter(x=>x.enabled).slice(0,1).map(x=>x.id)}};
renderCreate=function(){const tg=settings?.telegram?.channels||[];view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Пост</p><h2>Редактор публикации</h2></div><select id="mode" onchange="mode=this.value"><option value="auto" ${mode==='auto'?'selected':''}>Автогенерация</option><option value="manual" ${mode==='manual'?'selected':''}>Вручную</option></select></div><div class="row"><label>Тема новости<input id="topic" value="${esc(currentNews?.title||'Запуск сервиса автоматического постинга новостей')}"></label><label>Тон<select id="tone"><option>деловой</option><option>живой</option><option>экспертный</option></select></label><button class="action" onclick="generate()">Сгенерировать</button></div><label>Заголовок<input id="title" value="${esc(currentNews?.title||'Запуск МногоПост')}"></label><label>Текст<textarea id="text">${esc(currentNews?(currentNews.content_excerpt||'')+'\\n\\nИсточник: '+currentNews.url:'Сервис готовит публикации для нескольких соцсетей, адаптирует текст под лимиты каналов и ставит посты в очередь.')}</textarea></label><div class="row"><label>Ссылка<input id="link" value="${esc(currentNews?.url||'')}" placeholder="https://..."></label><label>Media URL<input id="media" placeholder="https://.../image.jpg"></label><label>Дата<input id="date" type="datetime-local"></label></div><p><button class="secondary" onclick="save()">Сохранить черновик</button> <button class="secondary" onclick="save('pending_approval')">На согласование</button> <button class="action" onclick="save('scheduled')">Добавить в очередь</button></p></div><div class="panel"><p class="eyebrow">Соцсети</p><h2>Куда публикуем</h2><div class="pick">${channels.map(c=>`<button class="channel ${selected.includes(c.id)?'selected':''}" onclick="toggle('${c.id}')"><span class="icon ${c.tone}">${c.short}</span><span><b>${esc(c.name)}</b><br><small>${esc(c.feature)}</small></span></button>`).join('')}</div><div class="panel" style="box-shadow:none;margin-top:14px"><p class="eyebrow">Telegram-каналы</p><div class="list">${tg.map(t=>`<label><input type="checkbox" ${tgSelected.includes(t.id)?'checked':''} onchange="toggleTg('${t.id}')"> ${esc(t.name)} <span class="muted">${esc(t.chat_id)}</span></label>`).join('')}</div></div></div></section>`};
toggleTg=function(id){tgSelected=tgSelected.includes(id)?tgSelected.filter(x=>x!==id):[...tgSelected,id]};
generate=async function(){const d=await api('/api/generate-post',{method:'POST',body:JSON.stringify({topic:topic.value,tone:tone.value,channels:selected,link:link.value,excerpt:text.value})});title.value=d.generated.title;text.value=d.generated.main_text;note('Черновик сгенерирован по текущим правилам.')};
save=async function(status){const d=await api('/api/posts',{method:'POST',body:JSON.stringify({title:title.value,text:text.value,mode,channels:selected,telegram_targets:tgSelected,scheduled_at:date.value?new Date(date.value).toISOString():null,media_urls:media.value?[media.value]:[],news_item_id:currentNews?.id||null})});if(status==='scheduled')await api(`/api/posts/${d.post.id}/schedule`,{method:'POST',body:JSON.stringify({scheduled_at:date.value?new Date(date.value).toISOString():new Date().toISOString()})});if(status==='pending_approval')await api(`/api/posts/${d.post.id}/approve`,{method:'POST',body:'{}'});currentNews=null;await refresh();tab='Публикации';renderNav();render();note(status==='scheduled'?'Пост добавлен в очередь.':'Черновик сохранен.')};
renderPosts=function(){view.innerHTML=`<section class="panel"><div class="title"><div><p class="eyebrow">Посты</p><h2>Очередь и статусы</h2></div><button class="action" onclick="publishDue()">Запустить планировщик</button></div><div class="list">${posts.length?posts.map(p=>`<article class="post"><div><time>${p.scheduled_at?new Date(p.scheduled_at).toLocaleString('ru-RU'):'без даты'}</time><h3>${esc(p.title)}</h3><p class="preview">${esc(p.text)}</p>${p.channels.map(c=>`<span class="chip">${esc(c.display_name||c.channel)}: ${labels[c.status]||esc(c.status)}</span>`).join('')}</div><div class="actions"><span class="pill">${labels[p.status]||p.status}</span><button class="secondary" onclick="act('${p.id}','approve')">Согласовать</button><button class="secondary" onclick="act('${p.id}','schedule')">В очередь</button><button class="secondary" onclick="act('${p.id}','publish-now')">Опубликовать</button></div></article>`).join(''):'<p class="muted">Пока нет постов.</p>'}</div></section>`};
renderSources=function(){view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">RSS</p><h2>Источники новостей</h2></div><button class="action" onclick="importNews()">Импортировать</button></div><div class="row"><label>Название<input id="sourceName" value="Новости"></label><label>RSS URL<input id="sourceUrl" placeholder="https://site.ru/rss.xml"></label><button class="secondary" onclick="addSource()">Добавить</button></div><div class="list">${sources.map(s=>`<div class="card"><b>${esc(s.name)}</b><p class="muted">${esc(s.url)}</p><small>${esc(s.status)}${s.last_error?' / '+esc(s.last_error):''}</small></div>`).join('')||'<p class="muted">Пока нет источников.</p>'}</div></div><div class="panel"><p class="eyebrow">Импорт</p><h2>Найденные новости</h2><div class="list">${news.map(n=>`<div class="card"><b>${esc(n.title)}</b><p class="muted">${esc(n.content_excerpt||n.url)}</p><button class="secondary" onclick='draftFromNews(${JSON.stringify(n).replace(/'/g,''')})'>В редактор</button> <button class="action" onclick='quickQueue(${JSON.stringify(n).replace(/'/g,''')})'>В очередь</button></div>`).join('')||'<p class="muted">После импорта здесь появятся новости.</p>'}</div></div></section>`};
draftFromNews=function(n){currentNews=n;tab='Создать пост';renderNav();render()};
quickQueue=async function(n){currentNews=n;tab='Создать пост';renderNav();render();await generate();await save(settings.rules?.generation?.approval_mode==='auto'?'scheduled':'pending_approval')};
renderChannels=function(){const tg=settings?.telegram?.channels||[];view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Интеграции</p><h2>API-готовность</h2><div class="pick">${channels.map(c=>{let cn=connections.find(x=>x.channel===c.id)||{};return `<article class="card"><span class="icon ${c.tone}">${c.short}</span><h3>${esc(c.name)}</h3><p class="muted">${esc(cn.runtime_message||c.feature)}</p><b>${labels[cn.runtime_status||cn.status]||'не подключено'}</b></article>`}).join('')}</div></div><div class="panel"><p class="eyebrow">Telegram</p><h2>Каналы</h2><div class="list">${tg.map(t=>`<article class="card"><b>${esc(t.name)}</b><p class="muted">${esc(t.chat_id)} ${t.is_default?' / по умолчанию':''}</p><button class="secondary" onclick="checkTelegram('${t.id}')">Проверить</button></article>`).join('')}</div></div></section>`};
renderSettings=function(){const tg=settings?.telegram?.channels||[];const gen=settings?.rules?.generation||{}, sch=settings?.rules?.schedule||{};view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Telegram</p><h2>Бот и каналы</h2><p class="muted">Токен хранится вне public_html. В чат он не выводится, видна только маска.</p><label>Bot token<input id="tgToken" placeholder="${settings?.telegram?.bot_token_masked||'123456:ABC...'}"></label><label>Chat ID по умолчанию<input id="tgChat" placeholder="${settings?.telegram?.chat_id_masked||'@channel или -100...'}"></label><p><button class="action" onclick="saveTelegram()">Сохранить Telegram</button> <button class="secondary" onclick="checkTelegram()">Проверить default</button></p><h3>Список каналов</h3><div class="list" id="tgChannelList">${tg.map((t,i)=>`<div class="row"><input value="${esc(t.name)}" data-k="name" data-i="${i}"><input value="${esc(t.chat_id)}" data-k="chat_id" data-i="${i}"><label><input type="checkbox" data-k="enabled" data-i="${i}" ${t.enabled?'checked':''}> активен</label></div>`).join('')}</div><p><button class="secondary" onclick="saveTgChannels()">Сохранить каналы</button></p></div><div class="panel"><p class="eyebrow">Правила</p><h2>Генерация и расписание</h2><label>Тон<input id="ruleTone" value="${esc(gen.tone||'деловой')}"></label><label>Длина<select id="ruleLength"><option value="short">коротко</option><option value="medium" ${(gen.length||'medium')==='medium'?'selected':''}>средне</option><option value="long" ${gen.length==='long'?'selected':''}>длинно</option></select></label><label>Хэштеги<input id="ruleTags" value="${esc(gen.hashtags||'#новости #многопост')}"></label><label><input id="ruleSource" type="checkbox" ${gen.append_source?'checked':''}> добавлять источник</label><label>Режим<select id="ruleApproval"><option value="manual">на согласование</option><option value="auto" ${gen.approval_mode==='auto'?'selected':''}>автоочередь</option></select></label><label>Постов в день<input id="postsPerDay" type="number" min="1" max="24" value="${esc(sch.posts_per_day||3)}"></label><label>Время публикаций<input id="timeWindows" value="${esc((sch.time_windows||['10:00','14:00','18:00']).join(', '))}"></label><button class="action" onclick="saveRules()">Сохранить правила</button></div><div class="panel"><p class="eyebrow">Автозапуск</p><h2>Cron URL</h2><p class="muted">Поставьте этот URL в планировщик SpaceWeb или внешний cron: раз в 5 минут.</p><input readonly value="${settings?.cron?.url||''}" onclick="this.select()"><p class="muted">Последний запуск: ${settings?.cron?.last_cron_at||'еще не запускался'}<br>PHP: ${settings?.runtime?.php_version||''}<br>Хранилище: ${settings?.runtime?.store_path||''}</p><button class="secondary" onclick="runCron()">Запустить cron сейчас</button></div></section>`};
saveTgChannels=async function(){const base=settings.telegram.channels||[];document.querySelectorAll('#tgChannelList input').forEach(el=>{const i=+el.dataset.i,k=el.dataset.k;base[i][k]=k==='enabled'?el.checked:el.value});await api('/api/settings/telegram-channels',{method:'POST',body:JSON.stringify({channels:base})});await refresh();note('Список Telegram-каналов сохранен.')};
saveRules=async function(){await api('/api/settings/rules',{method:'POST',body:JSON.stringify({generation:{tone:ruleTone.value,length:ruleLength.value,append_source:ruleSource.checked,hashtags:ruleTags.value,approval_mode:ruleApproval.value},schedule:{posts_per_day:postsPerDay.value,time_windows:timeWindows.value.split(',').map(x=>x.trim()).filter(Boolean),mode:'queue'}})});await refresh();note('Правила сохранены.')};
checkTelegram=async function(id){try{const d=await api('/api/telegram/check',{method:'POST',body:JSON.stringify(id?{telegram_channel_id:id}:{})});await refresh();note(d.message||'Проверка выполнена.')}catch(e){note('Ошибка проверки Telegram: '+e.message)}};
renderLogs=function(){view.innerHTML=`<section class="panel"><p class="eyebrow">Логи</p><h2>Журнал публикаций</h2><div class="list">${logs.map(l=>`<article class="log"><time>${new Date(l.created_at).toLocaleString('ru-RU')}</time> <b>${esc(l.channel)}</b> <span class="pill ${l.status==='error'?'danger':''}">${esc(l.action)} / ${esc(l.status)}</span><p>${esc(l.message)}</p></article>`).join('')||'<p class="muted">Пока нет событий.</p>'}</div></section>`};
</script>
<script>
if(!tabs.includes('Готовность')) tabs.unshift('Готовность');
var readiness=null;
var oldRender=render;
render=function(){if(tab==='Готовность')return renderReadiness();return oldRender()};
renderNav();
act=async function(id,a){if(a==='publish-now'&&!confirm('Опубликовать пост сейчас в подключенные каналы?'))return;await api(`/api/posts/${id}/${a}`,{method:'POST',body:'{}'});await refresh();note('Действие выполнено.')};
runCron=async function(){const d=await api('/api/cron/run',{method:'POST',body:'{}'});await refresh();note('Cron выполнен. Обработано публикаций: '+d.published)};
publishDue=async function(){const d=await api('/api/publish-due',{method:'POST',body:'{}'});await refresh();note('Планировщик обработал публикаций: '+d.count)};
function statusPill(ok, text){return `<span class="pill ${ok?'':'danger'}">${esc(text)}</span>`}
function renderReadiness(){view.innerHTML='<section class="panel"><p class="eyebrow">Проверка</p><h2>Собираю состояние сервиса...</h2></section>';api('/api/diagnostics').then(d=>{readiness=d;const tg=d.telegram||{}, rss=d.rss||{}, cron=d.cron||{}, socials=d.socials||[], blockers=d.blockers||[];view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Готовность</p><h2>Что уже работает</h2></div><button class="action" onclick="renderReadiness()">Обновить</button></div><div class="list"><article class="card"><h3>Telegram</h3><p>${statusPill(tg.configured,'бот '+(tg.configured?'настроен':'не настроен'))} ${statusPill((tg.ready_channels||0)>0,'каналов готово: '+(tg.ready_channels||0)+'/'+(tg.channels_count||0))}</p><p class="muted">${esc(tg.last_error||'Права проверяются по кнопке в разделе каналов.')}</p><button class="secondary" onclick="checkAllTelegram()">Проверить все каналы</button></article><article class="card"><h3>RSS-источники</h3><p>${statusPill((rss.active_sources||0)>0,'активных: '+(rss.active_sources||0)+'/'+(rss.sources_count||0))}</p><p class="muted">${esc(rss.last_error||'Источники сохранены. Импорт зависит от исходящего HTTPS на SpaceWeb.')}</p><button class="secondary" onclick="importNews()">Импортировать новости</button></article><article class="card"><h3>Планировщик</h3><p>${statusPill(cron.configured,'cron '+(cron.configured?'настроен':'не настроен'))}</p><p class="muted">Последний запуск: ${esc(cron.last_cron_at||'еще не запускался')}</p><button class="secondary" onclick="runCron()">Запустить сейчас</button></article></div></div><div class="panel"><p class="eyebrow">Блокеры</p><h2>Что мешает боевому запуску</h2><div class="list">${blockers.length?blockers.map(b=>`<article class="card"><b>${esc(b.title||'Проблема')}</b><p class="muted">${esc(b.message||'')}</p></article>`).join(''):'<p class="muted">Критичных блокеров по локальной диагностике нет.</p>'}</div><h3>Соцсети</h3><div class="list">${socials.map(s=>`<div class="card"><b>${esc(s.name)}</b><p>${statusPill(s.runtime_status==='connected',labels[s.runtime_status]||s.runtime_status||'не подключено')}</p><p class="muted">${esc(s.runtime_message||'')}</p></div>`).join('')}</div></div></section>`}).catch(e=>{view.innerHTML=`<section class="panel"><p class="eyebrow">Ошибка</p><h2>Диагностика недоступна</h2><p class="muted">${esc(e.message)}</p></section>`})}
renderSources=function(){view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">RSS</p><h2>Источники новостей</h2></div><button class="action" onclick="importNews()">Импортировать</button></div><div class="row"><label>Название<input id="sourceName" value="Новости"></label><label>RSS URL<input id="sourceUrl" placeholder="https://site.ru/rss.xml"></label><button class="secondary" onclick="addSource()">Добавить</button></div><div class="list">${sources.map(s=>`<div class="card"><b>${esc(s.name)}</b><p class="muted">${esc(s.url)}</p><small>${esc(s.status)}${s.last_error?' / '+esc(s.last_error):''}</small></div>`).join('')||'<p class="muted">Пока нет источников.</p>'}</div></div><div class="panel"><p class="eyebrow">Ручная новость</p><h2>Добавить без RSS</h2><label>Заголовок<input id="manualTitle" placeholder="Заголовок новости"></label><label>Ссылка<input id="manualUrl" placeholder="https://..."></label><label>Краткое описание<textarea id="manualExcerpt" style="min-height:120px"></textarea></label><p><button class="action" onclick="addManualNews()">Добавить новость</button></p><p class="eyebrow">Импорт</p><h2>Найденные новости</h2><div class="list">${news.map(n=>`<div class="card"><b>${esc(n.title)}</b><p class="muted">${esc(n.content_excerpt||n.url)}</p><button class="secondary" onclick='draftFromNews(${JSON.stringify(n).replace(/'/g,''')})'>В редактор</button> <button class="action" onclick='quickQueue(${JSON.stringify(n).replace(/'/g,''')})'>В очередь</button></div>`).join('')||'<p class="muted">После импорта здесь появятся новости.</p>'}</div></div></section>`};
addManualNews=async function(){await api('/api/news/items',{method:'POST',body:JSON.stringify({title:manualTitle.value,url:manualUrl.value,content_excerpt:manualExcerpt.value})});await refresh();note('Новость добавлена вручную.')};
checkAllTelegram=async function(){try{const d=await api('/api/telegram/check-all',{method:'POST',body:'{}'});await refresh();tab='Каналы';renderNav();render();note('Проверено Telegram-каналов: '+(d.results||[]).length)}catch(e){note('Ошибка проверки Telegram: '+e.message)}};
renderChannels=function(){const tg=settings?.telegram?.channels||[];view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Интеграции</p><h2>API-готовность</h2></div><button class="action" onclick="checkAllTelegram()">Проверить Telegram</button></div><div class="pick">${channels.map(c=>{let cn=connections.find(x=>x.channel===c.id)||{};return `<article class="card"><span class="icon ${c.tone}">${c.short}</span><h3>${esc(c.name)}</h3><p class="muted">${esc(cn.runtime_message||c.feature)}</p><b>${labels[cn.runtime_status||cn.status]||'не подключено'}</b></article>`}).join('')}</div></div><div class="panel"><p class="eyebrow">Telegram</p><h2>Каналы</h2><div class="list">${tg.map(t=>`<article class="card"><b>${esc(t.name)}</b><p class="muted">${esc(t.chat_id)} ${t.is_default?' / по умолчанию':''}</p><p>${statusPill(t.last_status==='ok',t.last_status||'не проверен')} ${t.bot_member_status?'<span class="chip">'+esc(t.bot_member_status)+'</span>':''}</p>${t.last_error?'<p class="muted">'+esc(t.last_error)+'</p>':''}<button class="secondary" onclick="checkTelegram('${t.id}')">Проверить</button></article>`).join('')}</div></div></section>`};
renderSettings=function(){const tg=settings?.telegram?.channels||[];const gen=settings?.rules?.generation||{}, sch=settings?.rules?.schedule||{};view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Telegram</p><h2>Бот и каналы</h2><p class="muted">Токен хранится вне public_html. В интерфейсе видна только маска.</p><label>Bot token<input id="tgToken" placeholder="${settings?.telegram?.bot_token_masked||'123456:ABC...'}"></label><label>Chat ID по умолчанию<input id="tgChat" placeholder="${settings?.telegram?.chat_id_masked||'@channel или -100...'}"></label><p><button class="action" onclick="saveTelegram()">Сохранить Telegram</button> <button class="secondary" onclick="checkTelegram()">Проверить default</button></p><h3>Список каналов</h3><div class="list" id="tgChannelList">${tg.map((t,i)=>`<div class="row"><input value="${esc(t.name)}" data-k="name" data-i="${i}"><input value="${esc(t.chat_id)}" data-k="chat_id" data-i="${i}"><label><input type="checkbox" data-k="enabled" data-i="${i}" ${t.enabled?'checked':''}> активен</label></div>`).join('')}</div><p><button class="secondary" onclick="saveTgChannels()">Сохранить каналы</button></p></div><div class="panel"><p class="eyebrow">Правила</p><h2>Генерация и расписание</h2><label>Тон<input id="ruleTone" value="${esc(gen.tone||'деловой')}"></label><label>Длина<select id="ruleLength"><option value="short">коротко</option><option value="medium" ${(gen.length||'medium')==='medium'?'selected':''}>средне</option><option value="long" ${gen.length==='long'?'selected':''}>длинно</option></select></label><label>Хэштеги<input id="ruleTags" value="${esc(gen.hashtags||'#новости #многопост')}"></label><label><input id="ruleSource" type="checkbox" ${gen.append_source?'checked':''}> добавлять источник</label><label>Режим<select id="ruleApproval"><option value="manual">на согласование</option><option value="auto" ${gen.approval_mode==='auto'?'selected':''}>автоочередь</option></select></label><label>Постов в день<input id="postsPerDay" type="number" min="1" max="24" value="${esc(sch.posts_per_day||3)}"></label><label>Время публикаций<input id="timeWindows" value="${esc((sch.time_windows||['10:00','14:00','18:00']).join(', '))}"></label><button class="action" onclick="saveRules()">Сохранить правила</button></div><div class="panel"><p class="eyebrow">VK</p><h2>Заготовка интеграции</h2><label>Access token<input id="vkToken" placeholder="vk1.a..."></label><label>Group ID<input id="vkGroup" placeholder="-123456789"></label><p><button class="secondary" onclick="saveVk()">Сохранить VK</button> <button class="secondary" onclick="checkVk()">Проверить VK</button></p><p class="muted">Публикация VK останется ручной, пока не добавлен валидный токен группы с правами wall.</p></div><div class="panel"><p class="eyebrow">Автозапуск</p><h2>Cron URL</h2><p class="muted">URL скрыт в интерфейсе. Для ручного запуска используется безопасный endpoint админки.</p><input readonly value="${settings?.cron?.url_masked||'cron скрыт'}" onclick="this.select()"><p class="muted">Последний запуск: ${settings?.cron?.last_cron_at||'еще не запускался'}<br>PHP: ${settings?.runtime?.php_version||''}<br>Хранилище: ${settings?.runtime?.store_path||''}</p><button class="secondary" onclick="runCron()">Запустить cron сейчас</button></div></section>`};
saveVk=async function(){await api('/api/settings/vk',{method:'POST',body:JSON.stringify({access_token:vkToken.value,group_id:vkGroup.value})});await refresh();note('VK-настройки сохранены.')};
checkVk=async function(){try{const d=await api('/api/vk/check',{method:'POST',body:'{}'});note(d.message||'VK проверен.')}catch(e){note('VK пока не готов: '+e.message)}};
</script>
<script>
renderReadiness=function(){view.innerHTML='<section class="panel"><p class="eyebrow">Проверка</p><h2>Собираю состояние сервиса...</h2></section>';api('/api/diagnostics').then(d=>{readiness=d;const tg=d.telegram||{}, rss=d.rss||{}, cron=d.cron||{};const blockers=Array.isArray(d.blockers)?d.blockers:[];const socials=Array.isArray(d.socials)?d.socials:Object.entries(d.socials||{}).map(([id,status])=>({id,name:(channels.find(c=>c.id===id)||{}).name||id,runtime_status:status,runtime_message:''}));const readyChannels=tg.ready_channels??tg.ready_count??0;const rssActive=rss.active_sources??rss.active_count??0;const cronLast=cron.last_cron_at??cron.last_run??null;view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Готовность</p><h2>Что уже работает</h2></div><button class="action" onclick="renderReadiness()">Обновить</button></div><div class="list"><article class="card"><h3>Telegram</h3><p>${statusPill(!!tg.configured,'бот '+(tg.configured?'настроен':'не настроен'))} ${statusPill(readyChannels>0,'каналов готово: '+readyChannels+'/'+(tg.channels_count||0))}</p><p class="muted">${esc(tg.last_error||'Права проверяются по кнопке в разделе каналов.')}</p><button class="secondary" onclick="checkAllTelegram()">Проверить все каналы</button></article><article class="card"><h3>RSS-источники</h3><p>${statusPill(rssActive>0,'активных: '+rssActive+'/'+(rss.sources_count||0))}</p><p class="muted">${esc(rss.last_error||d.network?.rss?.last_error||'Источники сохранены. Импорт зависит от исходящего HTTPS на SpaceWeb.')}</p><button class="secondary" onclick="importNews()">Импортировать новости</button></article><article class="card"><h3>Планировщик</h3><p>${statusPill(!!cron.configured,'cron '+(cron.configured?'настроен':'не настроен'))}</p><p class="muted">Последний запуск: ${esc(cronLast||'еще не запускался')}</p><button class="secondary" onclick="runCron()">Запустить сейчас</button></article></div></div><div class="panel"><p class="eyebrow">Блокеры</p><h2>Что мешает боевому запуску</h2><div class="list">${blockers.length?blockers.map(b=>{const title=typeof b==='string'?'Проблема':(b.title||'Проблема');const message=typeof b==='string'?b:(b.message||'');return `<article class="card"><b>${esc(title)}</b><p class="muted">${esc(message)}</p></article>`}).join(''):'<p class="muted">Критичных блокеров по локальной диагностике нет.</p>'}</div><h3>Соцсети</h3><div class="list">${socials.map(s=>`<div class="card"><b>${esc(s.name)}</b><p>${statusPill(s.runtime_status==='connected'||s.runtime_status==='configured',labels[s.runtime_status]||s.runtime_status||'не подключено')}</p><p class="muted">${esc(s.runtime_message||'')}</p></div>`).join('')}</div></div></section>`}).catch(e=>{view.innerHTML=`<section class="panel"><p class="eyebrow">Ошибка</p><h2>Диагностика недоступна</h2><p class="muted">${esc(e.message)}</p></section>`})};
</script>
<script>
runAutopilot=async function(){const d=await api('/api/autopilot/run',{method:'POST',body:'{}'});await refresh();note('Автопилот: импорт '+d.imported+', в очередь '+d.queued)};
renderReadiness=function(){view.innerHTML='<section class="panel"><p class="eyebrow">Проверка</p><h2>Собираю состояние сервиса...</h2></section>';api('/api/diagnostics').then(d=>{readiness=d;const tg=d.telegram||{}, rss=d.rss||{}, cron=d.cron||{}, auto=d.autopilot||{};const blockers=Array.isArray(d.blockers)?d.blockers:[];const socials=Array.isArray(d.socials)?d.socials:Object.entries(d.socials||{}).map(([id,status])=>({id,name:(channels.find(c=>c.id===id)||{}).name||id,runtime_status:status,runtime_message:''}));const rssActive=rss.active_sources??rss.active_count??0;const cronLast=cron.last_cron_at??cron.last_run??null;view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Готовность</p><h2>Автопостинг</h2></div><button class="action" onclick="renderReadiness()">Обновить</button></div><div class="list"><article class="card"><h3>Автопилот</h3><p>${statusPill(!!auto.enabled,'автопилот '+(auto.enabled?'включен':'выключен'))} ${statusPill((auto.ready_news||0)>0,'новостей к обработке: '+(auto.ready_news||0))}</p><p class="muted">За запуск: ${esc(auto.max_posts_per_run||3)}. Импорт по cron: ${auto.import_on_cron?'да':'нет'}.</p><button class="secondary" onclick="runAutopilot()">Запустить автопилот</button></article><article class="card"><h3>Telegram</h3><p>${statusPill(!!tg.configured,'бот '+(tg.configured?'настроен':'не настроен'))} ${statusPill((tg.channels_count||0)>0,'каналов: '+(tg.channels_count||0))}</p><p class="muted">${esc(tg.last_error||'Права проверяются по кнопке в разделе каналов.')}</p><button class="secondary" onclick="checkAllTelegram()">Проверить все каналы</button></article><article class="card"><h3>RSS-источники</h3><p>${statusPill(rssActive>0,'активных: '+rssActive+'/'+(rss.sources_count||0))}</p><p class="muted">${esc(rss.last_error||d.network?.rss?.last_error||'Источники сохранены. Импорт зависит от исходящего HTTPS на SpaceWeb.')}</p><button class="secondary" onclick="importNews()">Импортировать новости</button></article><article class="card"><h3>Планировщик</h3><p>${statusPill(!!cron.configured,'cron '+(cron.configured?'настроен':'не настроен'))}</p><p class="muted">Последний запуск: ${esc(cronLast||'еще не запускался')}</p><button class="secondary" onclick="runCron()">Запустить сейчас</button></article></div></div><div class="panel"><p class="eyebrow">Блокеры</p><h2>Что мешает боевому запуску</h2><div class="list">${blockers.length?blockers.map(b=>{const title=typeof b==='string'?'Проблема':(b.title||'Проблема');const message=typeof b==='string'?b:(b.message||'');return `<article class="card"><b>${esc(title)}</b><p class="muted">${esc(message)}</p></article>`}).join(''):'<p class="muted">Критичных блокеров по локальной диагностике нет.</p>'}</div><h3>Соцсети</h3><div class="list">${socials.map(s=>`<div class="card"><b>${esc(s.name)}</b><p>${statusPill(s.runtime_status==='connected'||s.runtime_status==='configured',labels[s.runtime_status]||s.runtime_status||'не подключено')}</p><p class="muted">${esc(s.runtime_message||'')}</p></div>`).join('')}</div></div></section>`}).catch(e=>{view.innerHTML=`<section class="panel"><p class="eyebrow">Ошибка</p><h2>Диагностика недоступна</h2><p class="muted">${esc(e.message)}</p></section>`})};
renderSettings=function(){const tg=settings?.telegram?.channels||[];const gen=settings?.rules?.generation||{}, sch=settings?.rules?.schedule||{}, auto=settings?.rules?.autopilot||{};const autoChannels=auto.channels||['telegram'];const autoTg=auto.telegram_targets||[];view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Telegram</p><h2>Бот и каналы</h2><p class="muted">Токен хранится вне public_html. В интерфейсе видна только маска.</p><label>Bot token<input id="tgToken" placeholder="${settings?.telegram?.bot_token_masked||'123456:ABC...'}"></label><label>Chat ID по умолчанию<input id="tgChat" placeholder="${settings?.telegram?.chat_id_masked||'@channel или -100...'}"></label><p><button class="action" onclick="saveTelegram()">Сохранить Telegram</button> <button class="secondary" onclick="checkTelegram()">Проверить default</button></p><h3>Список каналов</h3><div class="list" id="tgChannelList">${tg.map((t,i)=>`<div class="row"><input value="${esc(t.name)}" data-k="name" data-i="${i}"><input value="${esc(t.chat_id)}" data-k="chat_id" data-i="${i}"><label><input type="checkbox" data-k="enabled" data-i="${i}" ${t.enabled?'checked':''}> активен</label></div>`).join('')}</div><p><button class="secondary" onclick="saveTgChannels()">Сохранить каналы</button></p></div><div class="panel"><p class="eyebrow">Автопилот</p><h2>Автоматическая очередь</h2><label><input id="autoEnabled" type="checkbox" ${auto.enabled?'checked':''}> включить автопостинг новостей</label><label><input id="autoImport" type="checkbox" ${auto.import_on_cron?'checked':''}> импортировать RSS при cron</label><label>Новостей за запуск<input id="autoMax" type="number" min="1" max="24" value="${esc(auto.max_posts_per_run||3)}"></label><p class="eyebrow">Соцсети автопилота</p><div class="list">${channels.map(c=>`<label><input class="autoChannel" value="${esc(c.id)}" type="checkbox" ${autoChannels.includes(c.id)?'checked':''}> ${esc(c.name)}</label>`).join('')}</div><p class="eyebrow">Telegram-цели автопилота</p><div class="list">${tg.map(t=>`<label><input class="autoTg" value="${esc(t.id)}" type="checkbox" ${autoTg.includes(t.id)?'checked':''}> ${esc(t.name)} <span class="muted">${esc(t.chat_id)}</span></label>`).join('')}</div><p><button class="action" onclick="saveRules()">Сохранить автопилот</button> <button class="secondary" onclick="runAutopilot()">Запустить сейчас</button></p></div><div class="panel"><p class="eyebrow">Правила</p><h2>Генерация и расписание</h2><label>Тон<input id="ruleTone" value="${esc(gen.tone||'деловой')}"></label><label>Длина<select id="ruleLength"><option value="short">коротко</option><option value="medium" ${(gen.length||'medium')==='medium'?'selected':''}>средне</option><option value="long" ${gen.length==='long'?'selected':''}>длинно</option></select></label><label>Хэштеги<input id="ruleTags" value="${esc(gen.hashtags||'#новости #многопост')}"></label><label><input id="ruleSource" type="checkbox" ${gen.append_source?'checked':''}> добавлять источник</label><label>Режим<select id="ruleApproval"><option value="manual">на согласование</option><option value="auto" ${gen.approval_mode==='auto'?'selected':''}>сразу в очередь</option></select></label><label>Постов в день<input id="postsPerDay" type="number" min="1" max="24" value="${esc(sch.posts_per_day||3)}"></label><label>Время публикаций<input id="timeWindows" value="${esc((sch.time_windows||['10:00','14:00','18:00']).join(', '))}"></label><label>Часовой пояс<input id="timeZone" value="${esc(sch.timezone||'Europe/Moscow')}"></label><button class="action" onclick="saveRules()">Сохранить правила</button></div><div class="panel"><p class="eyebrow">VK</p><h2>Публикация VK</h2><label>Access token<input id="vkToken" placeholder="vk1.a..."></label><label>Group ID<input id="vkGroup" placeholder="-123456789"></label><p><button class="secondary" onclick="saveVk()">Сохранить VK</button> <button class="secondary" onclick="checkVk()">Проверить VK</button></p><p class="muted">После сохранения токена сообщества с правом wall автопилот сможет публиковать во ВКонтакте.</p></div><div class="panel"><p class="eyebrow">Автозапуск</p><h2>Cron URL</h2><p class="muted">URL скрыт в интерфейсе. Cron импортирует новости, создает очередь и публикует наступившие посты.</p><input readonly value="${settings?.cron?.url_masked||'cron скрыт'}" onclick="this.select()"><p class="muted">Последний запуск: ${settings?.cron?.last_cron_at||'еще не запускался'}<br>PHP: ${settings?.runtime?.php_version||''}<br>Хранилище: ${settings?.runtime?.store_path||''}</p><button class="secondary" onclick="runCron()">Запустить cron сейчас</button></div></section>`};
saveRules=async function(){const autoChannels=[...document.querySelectorAll('.autoChannel:checked')].map(x=>x.value);const autoTg=[...document.querySelectorAll('.autoTg:checked')].map(x=>x.value);await api('/api/settings/rules',{method:'POST',body:JSON.stringify({generation:{tone:ruleTone.value,length:ruleLength.value,append_source:ruleSource.checked,hashtags:ruleTags.value,approval_mode:ruleApproval.value},schedule:{posts_per_day:postsPerDay.value,time_windows:timeWindows.value.split(',').map(x=>x.trim()).filter(Boolean),timezone:timeZone.value,mode:'queue'},autopilot:{enabled:autoEnabled.checked,import_on_cron:autoImport.checked,max_posts_per_run:autoMax.value,channels:autoChannels,telegram_targets:autoTg}})});await refresh();note('Правила автопостинга сохранены.')};
</script>
<script>
if(!tabs.includes('Ошибки')) tabs.splice(1,0,'Ошибки');
var supportPack=null;
var oldRenderOps=render;
render=function(){if(tab==='Готовность')return renderReadiness();if(tab==='Ошибки')return renderErrors();return oldRenderOps()};
function allTargets(){return posts.flatMap(p=>(p.channels||[]).map(t=>({...t,post:p})))}
function failedTargets(){return allTargets().filter(t=>['failed','manual_required'].includes(t.status)||t.last_error)}
function retryTargets(){return allTargets().filter(t=>t.next_retry_at)}
renderPosts=function(){const scheduled=posts.filter(p=>p.status==='scheduled').length, failed=failedTargets().length, pending=posts.filter(p=>p.status==='pending_approval').length, published=posts.filter(p=>p.status==='published').length;view.innerHTML=`<section class="panel"><div class="title"><div><p class="eyebrow">Очередь</p><h2>Публикации и автоновости</h2></div><p><button class="secondary" onclick="runAutopilot()">Автопилот</button> <button class="action" onclick="publishDue()">Планировщик</button></p></div><div class="ops"><div class="metric"><span class="muted">В очереди</span><b>${scheduled}</b></div><div class="metric"><span class="muted">На согласовании</span><b>${pending}</b></div><div class="metric"><span class="muted">Опубликовано</span><b>${published}</b></div><div class="metric"><span class="muted">Ошибки</span><b>${failed}</b></div></div><div class="list" style="margin-top:18px">${posts.length?posts.map(p=>`<article class="post"><div><time>${p.scheduled_at?new Date(p.scheduled_at).toLocaleString('ru-RU'):'без даты'}</time><h3>${esc(p.title)}</h3><p class="preview">${esc(p.text)}</p>${(p.channels||[]).map(c=>`<span class="chip ${c.status==='failed'?'danger':''}">${esc(c.display_name||c.channel)}: ${labels[c.status]||esc(c.status)}${c.next_retry_at?' / повтор '+new Date(c.next_retry_at).toLocaleString('ru-RU'):''}</span>`).join('')}</div><div class="actions"><span class="pill ${p.status==='failed'?'danger':''}">${labels[p.status]||p.status}</span><button class="secondary" onclick="act('${p.id}','approve')">Согласовать</button><button class="secondary" onclick="act('${p.id}','schedule')">В очередь</button><button class="secondary" onclick="act('${p.id}','publish-now')">Опубликовать</button></div></article>`).join(''):'<p class="muted">Пока нет постов.</p>'}</div></section>`};
renderErrors=function(){const failed=failedTargets(), retries=retryTargets();view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Ошибки</p><h2>Падения и повторы</h2></div><button class="action" onclick="runCron()">Повторить due</button></div><div class="ops"><div class="metric"><span class="muted">Ошибок</span><b>${failed.length}</b></div><div class="metric"><span class="muted">Ожидают retry</span><b>${retries.length}</b></div><div class="metric"><span class="muted">RSS ошибок</span><b>${sources.filter(s=>s.status==='error').length}</b></div><div class="metric"><span class="muted">Telegram ошибок</span><b>${(settings?.telegram?.channels||[]).filter(t=>t.last_error).length}</b></div></div><div class="list" style="margin-top:18px">${failed.length?failed.map(t=>`<article class="card"><b>${esc(t.post.title)}</b><p>${statusPill(false,esc(t.display_name||t.channel)+' / '+(labels[t.status]||t.status||'ошибка'))}</p><p class="muted">${esc(t.last_error||'Требуется ручное действие.')}</p>${t.next_retry_at?'<p class="muted">Следующая попытка: '+new Date(t.next_retry_at).toLocaleString('ru-RU')+'</p>':''}<button class="secondary" onclick="act('${t.post.id}','publish-now')">Повторить сейчас</button></article>`).join(''):'<p class="muted">Активных ошибок публикации нет.</p>'}</div></div><div class="panel"><p class="eyebrow">Источники и каналы</p><h2>Что проверить</h2><div class="list">${sources.filter(s=>s.status==='error').map(s=>`<article class="card"><b>${esc(s.name)}</b><p class="muted">${esc(s.last_error||s.url)}</p></article>`).join('')||'<p class="muted">RSS-ошибок в списке источников нет.</p>'}${(settings?.telegram?.channels||[]).filter(t=>t.last_error).map(t=>`<article class="card"><b>${esc(t.name)}</b><p class="muted">${esc(t.last_error)}</p></article>`).join('')}</div></div></section>`};
renderReadiness=function(){view.innerHTML='<section class="panel"><p class="eyebrow">Проверка</p><h2>Собираю состояние сервиса...</h2></section>';Promise.all([api('/api/diagnostics'),api('/api/support/spaceweb')]).then(([d,pack])=>{supportPack=pack;const tg=d.telegram||{}, rss=d.rss||{}, cron=d.cron||{}, auto=d.autopilot||{}, llm=d.llm||{};const blockers=Array.isArray(d.blockers)?d.blockers:[];view.innerHTML=`<section class="grid"><div class="panel"><div class="title"><div><p class="eyebrow">Цепочка</p><h2>Импорт → генерация → очередь → публикация</h2></div><p><button class="secondary" onclick="runAutopilot()">Автопилот</button> <button class="action" onclick="runCron()">Cron</button></p></div><div class="ops"><div class="metric"><span class="muted">Автопилот</span><b>${auto.enabled?'ON':'OFF'}</b></div><div class="metric"><span class="muted">RSS active</span><b>${rss.active_count??0}/${rss.sources_count??0}</b></div><div class="metric"><span class="muted">LLM</span><b>${llm.configured?'OK':'KEY'}</b></div><div class="metric"><span class="muted">Блокеры</span><b>${blockers.length}</b></div></div><div class="steps" style="margin-top:18px"><article class="card"><b>Импорт новостей</b><p class="muted">Cron берет RSS, чистит HTML, считает hash и отсекает дубли.</p></article><article class="card"><b>Генерация</b><p class="muted">Сейчас работает локальный fallback. После LLM-ключа можно включить внешнюю генерацию.</p></article><article class="card"><b>Очередь</b><p class="muted">Посты раскладываются по окнам и лимиту ${esc(settings?.rules?.schedule?.posts_per_day||3)} в день.</p></article><article class="card"><b>Публикация</b><p class="muted">Telegram и VK идут через API, остальные каналы остаются ручными до официальных ключей.</p></article></div></div><div class="panel"><p class="eyebrow">SpaceWeb</p><h2>Что сейчас мешает</h2><div class="list">${blockers.length?blockers.map(b=>`<article class="card"><b>Сетевой блокер</b><p class="muted">${esc(typeof b==='string'?b:b.message)}</p></article>`).join(''):'<p class="muted">Критичных блокеров нет.</p>'}</div><label>Текст в поддержку<textarea id="supportText" class="copybox">${esc(pack.text)}</textarea></label><p><button class="secondary" onclick="supportText.select();document.execCommand('copy');note('Текст обращения скопирован.')">Скопировать обращение</button></p><h3>Fallback worker/VPS</h3><div class="steps">${pack.fallback.map(x=>`<article class="card"><b>${esc(x.step)}</b><p class="muted">${esc(x.result)}</p></article>`).join('')}</div></div></section>`}).catch(e=>{view.innerHTML=`<section class="panel"><p class="eyebrow">Ошибка</p><h2>Диагностика недоступна</h2><p class="muted">${esc(e.message)}</p></section>`})};
renderSettings=function(){const tg=settings?.telegram?.channels||[];const gen=settings?.rules?.generation||{}, sch=settings?.rules?.schedule||{}, auto=settings?.rules?.autopilot||{}, llm=settings?.llm||{};const autoChannels=auto.channels||['telegram'];const autoTg=auto.telegram_targets||[];view.innerHTML=`<section class="grid"><div class="panel"><p class="eyebrow">Автопилот</p><h2>Автоматическая очередь</h2><label><input id="autoEnabled" type="checkbox" ${auto.enabled?'checked':''}> включить автопостинг новостей</label><label><input id="autoImport" type="checkbox" ${auto.import_on_cron?'checked':''}> импортировать RSS при cron</label><label>Новостей за запуск<input id="autoMax" type="number" min="1" max="24" value="${esc(auto.max_posts_per_run||3)}"></label><p class="eyebrow">Соцсети автопилота</p><div class="list">${channels.map(c=>`<label><input class="autoChannel" value="${esc(c.id)}" type="checkbox" ${autoChannels.includes(c.id)?'checked':''}> ${esc(c.name)}</label>`).join('')}</div><p class="eyebrow">Telegram-цели</p><div class="list">${tg.map(t=>`<label><input class="autoTg" value="${esc(t.id)}" type="checkbox" ${autoTg.includes(t.id)?'checked':''}> ${esc(t.name)}</label>`).join('')}</div><p><button class="action" onclick="saveRules()">Сохранить</button> <button class="secondary" onclick="runAutopilot()">Запустить</button></p></div><div class="panel"><p class="eyebrow">LLM</p><h2>Генерация через API</h2><label>Провайдер<select id="llmProvider"><option value="openai" ${llm.provider==='openai'?'selected':''}>OpenAI</option><option value="custom" ${llm.provider==='custom'?'selected':''}>Custom endpoint</option></select></label><label>Модель<input id="llmModel" value="${esc(llm.model||'gpt-4o-mini')}"></label><label>API key<input id="llmKey" type="password" placeholder="${esc(llm.api_key_masked||'sk-...')}"></label><p>${statusPill(!!llm.configured,llm.configured?'ключ сохранен':'нужен ключ')}</p><p><button class="action" onclick="saveLlm()">Сохранить LLM</button> <button class="secondary" onclick="checkLlm()">Проверить</button></p></div><div class="panel"><p class="eyebrow">Правила</p><h2>Генерация и расписание</h2><label>Тон<input id="ruleTone" value="${esc(gen.tone||'деловой')}"></label><label>Длина<select id="ruleLength"><option value="short">коротко</option><option value="medium" ${(gen.length||'medium')==='medium'?'selected':''}>средне</option><option value="long" ${gen.length==='long'?'selected':''}>длинно</option></select></label><label>Хэштеги<input id="ruleTags" value="${esc(gen.hashtags||'#новости #многопост')}"></label><label><input id="ruleSource" type="checkbox" ${gen.append_source?'checked':''}> добавлять источник</label><label>Режим<select id="ruleApproval"><option value="manual">на согласование</option><option value="auto" ${gen.approval_mode==='auto'?'selected':''}>сразу в очередь</option></select></label><label>Постов в день<input id="postsPerDay" type="number" min="1" max="24" value="${esc(sch.posts_per_day||3)}"></label><label>Время публикаций<input id="timeWindows" value="${esc((sch.time_windows||['10:00','14:00','18:00']).join(', '))}"></label><label>Часовой пояс<input id="timeZone" value="${esc(sch.timezone||'Europe/Moscow')}"></label><button class="action" onclick="saveRules()">Сохранить правила</button></div><div class="panel"><p class="eyebrow">Telegram</p><h2>Бот и каналы</h2><label>Bot token<input id="tgToken" placeholder="${settings?.telegram?.bot_token_masked||'123456:ABC...'}"></label><label>Chat ID по умолчанию<input id="tgChat" placeholder="${settings?.telegram?.chat_id_masked||'@channel или -100...'}"></label><p><button class="secondary" onclick="saveTelegram()">Сохранить Telegram</button> <button class="secondary" onclick="checkAllTelegram()">Проверить все</button></p><div class="list" id="tgChannelList">${tg.map((t,i)=>`<div class="row"><input value="${esc(t.name)}" data-k="name" data-i="${i}"><input value="${esc(t.chat_id)}" data-k="chat_id" data-i="${i}"><label><input type="checkbox" data-k="enabled" data-i="${i}" ${t.enabled?'checked':''}> активен</label></div>`).join('')}</div><p><button class="secondary" onclick="saveTgChannels()">Сохранить каналы</button></p></div><div class="panel"><p class="eyebrow">VK</p><h2>Публикация VK</h2><label>Access token<input id="vkToken" type="password" placeholder="vk1.a..."></label><label>Group ID<input id="vkGroup" placeholder="-123456789"></label><p><button class="secondary" onclick="saveVk()">Сохранить VK</button> <button class="secondary" onclick="checkVk()">Проверить VK</button></p></div><div class="panel"><p class="eyebrow">Cron</p><h2>Автозапуск</h2><input readonly value="${settings?.cron?.url_masked||'cron скрыт'}" onclick="this.select()"><p class="muted">Последний запуск: ${settings?.cron?.last_cron_at||'еще не запускался'}</p><button class="secondary" onclick="runCron()">Запустить cron</button></div></section>`};
saveLlm=async function(){await api('/api/settings/llm',{method:'POST',body:JSON.stringify({provider:llmProvider.value,model:llmModel.value,api_key:llmKey.value})});await refresh();note('LLM-настройки сохранены.')};
checkLlm=async function(){try{const d=await api('/api/llm/check',{method:'POST',body:'{}'});note(d.message)}catch(e){note('LLM пока не готов: '+e.message)}};
</script>
</body>
</html>