Fix archive missing on manual publish; refresh VK token instructions

- Mark scheduled post as 'published' instead of deleting it when user clicks "Опубликовать сейчас", so it appears in the archive (parity with cron path)
- Surface VK warnings (e.g. photos posted as links when community token lacks photos right) in both the post-publish notification and the archive card
- Replace dead vkhost.github.io / VK Admin instructions with a community-token flow via group settings, since VK now blocks the old app (error 8)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-04-30 15:32:37 +03:00
parent e5a88665cd
commit 83d507571e
3 changed files with 71 additions and 16 deletions
+37
View File
@@ -776,6 +776,43 @@ try {
} }
break; break;
case 'mark_post_published':
$scheduledFile = __DIR__ . '/data/scheduled_posts.json';
$postId = $_POST['id'] ?? '';
$resultsJson = $_POST['results'] ?? '{}';
$results = json_decode($resultsJson, true) ?: [];
if (!$postId) {
echo json_encode(['error' => 'ID поста не указан']);
exit;
}
$posts = file_exists($scheduledFile) ? json_decode(file_get_contents($scheduledFile), true) ?: [] : [];
$found = false;
foreach ($posts as &$post) {
if (($post['id'] ?? null) === $postId && ($post['status'] ?? '') === 'pending') {
$post['status'] = 'published';
$post['published_at'] = date('Y-m-d H:i:s');
$post['results'] = $results;
$found = true;
break;
}
}
unset($post);
if (!$found) {
echo json_encode(['error' => 'Пост не найден или уже опубликован']);
exit;
}
if (file_put_contents($scheduledFile, json_encode($posts, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['error' => 'Не удалось сохранить']);
}
break;
case 'get_published_posts': case 'get_published_posts':
$scheduledFile = __DIR__ . '/data/scheduled_posts.json'; $scheduledFile = __DIR__ . '/data/scheduled_posts.json';
if (file_exists($scheduledFile)) { if (file_exists($scheduledFile)) {
+9 -8
View File
@@ -625,17 +625,18 @@ foreach ($channels as $ch) {
</div> </div>
<div class="form-group"> <div class="form-group">
<details class="vk-help" open> <details class="vk-help" open>
<summary>Как получить пользовательский токен (для загрузки фото)?</summary> <summary>Как получить токен сообщества (рекомендуется)?</summary>
<ol style="margin: 10px 0; padding-left: 20px; font-size: 0.9em;"> <ol style="margin: 10px 0; padding-left: 20px; font-size: 0.9em;">
<li>Перейдите на <a href="https://vkhost.github.io/" target="_blank">vkhost.github.io</a></li> <li>Откройте свою группу в VK → <strong>Управление</strong></li>
<li>Нажмите <strong>"VK Admin"</strong></li> <li>В меню справа выберите <strong>«Работа с API»</strong> → вкладка <strong>«Ключи доступа»</strong> → <strong>«Создать ключ»</strong></li>
<li>Разрешите доступ приложению</li> <li>Включите права: <strong>Управление сообществом</strong>, <strong>Сообщения сообщества</strong>, <strong>Фотографии</strong>, <strong>Стена</strong> (Wall)</li>
<li>Скопируйте <code>access_token</code> из адресной строки браузера</li> <li>Скопируйте созданный ключ и вставьте его в поле выше → «Сохранить»</li>
<li>Вставьте токен в поле выше и нажмите "Сохранить"</li>
</ol> </ol>
<p style="font-size: 0.85em; color: var(--text-secondary); margin-top: 10px;"> <p style="font-size: 0.85em; color: var(--text-secondary); margin-top: 10px;">
<strong>Важно:</strong> Пользовательский токен позволяет загружать фото напрямую в VK. <strong>Важно:</strong> ключ сообщества бессрочный и не блокируется VK. Он постит и грузит фото только в свою группу — для нескольких групп создайте отдельный ключ в каждой.
Community-токен (ключ сообщества) может только постить текст и ссылки. </p>
<p style="font-size: 0.8em; color: var(--text-secondary); margin-top: 8px;">
Старый способ через <a href="https://vkhost.github.io/" target="_blank">vkhost.github.io</a> / VK Admin сейчас не работает — VK заблокировал это приложение (ошибка <code>[8] Application is blocked</code>).
</p> </p>
</details> </details>
</div> </div>
+24 -7
View File
@@ -3679,14 +3679,24 @@ document.addEventListener('DOMContentLoaded', function() {
const anySuccess = Object.values(results).some(r => r.success); const anySuccess = Object.values(results).some(r => r.success);
if (anySuccess) { if (anySuccess) {
// Delete the scheduled post // Mark scheduled post as published so it appears in archive
const delForm = new FormData(); const markForm = new FormData();
delForm.append('action', 'delete_scheduled_post'); markForm.append('action', 'mark_post_published');
delForm.append('id', postId); markForm.append('id', postId);
await fetch('api.php', { method: 'POST', body: delForm }); markForm.append('results', JSON.stringify(results));
await fetch('api.php', { method: 'POST', body: markForm });
// Surface VK warnings (e.g. photos posted as links instead of attachments)
const warnings = Object.entries(results)
.filter(([, r]) => r.success && r.warning)
.map(([p, r]) => `${p.toUpperCase()}: ${r.warning}`);
if (warnings.length > 0) {
showNotification('Опубликовано с предупреждением — ' + warnings.join(' | '), 'warning');
} else {
showNotification(allSuccess ? 'Опубликовано!' : 'Частично опубликовано', allSuccess ? 'success' : 'warning'); showNotification(allSuccess ? 'Опубликовано!' : 'Частично опубликовано', allSuccess ? 'success' : 'warning');
}
loadScheduledPosts(); loadScheduledPosts();
loadPublishedPosts();
} else { } else {
const errors = Object.entries(results).map(([p, r]) => `${p}: ${r.error}`).join(', '); const errors = Object.entries(results).map(([p, r]) => `${p}: ${r.error}`).join(', ');
showNotification('Ошибка: ' + errors, 'error'); showNotification('Ошибка: ' + errors, 'error');
@@ -3734,10 +3744,16 @@ document.addEventListener('DOMContentLoaded', function() {
// Check results for success/error // Check results for success/error
const results = post.results || {}; const results = post.results || {};
let statusIcons = ''; let statusIcons = '';
let warningText = '';
Object.entries(results).forEach(([platform, result]) => { Object.entries(results).forEach(([platform, result]) => {
const icon = result.success ? '✓' : '✗'; const icon = result.success ? (result.warning ? '⚠' : '✓') : '✗';
const platformName = platform === 'telegram' ? 'TG' : platform === 'vk' ? 'VK' : platform; const platformName = platform === 'telegram' ? 'TG' : platform === 'vk' ? 'VK' : platform;
statusIcons += `<span class="result-${result.success ? 'success' : 'error'}" title="${result.error || 'OK'}">${platformName} ${icon}</span> `; const tip = result.error || result.warning || 'OK';
const cls = result.success ? (result.warning ? 'result-warning' : 'result-success') : 'result-error';
statusIcons += `<span class="${cls}" title="${escapeHtml(tip)}">${platformName} ${icon}</span> `;
if (result.warning) {
warningText = `${platformName}: ${result.warning}`;
}
}); });
// Collect all photo URLs for preview // Collect all photo URLs for preview
@@ -3766,6 +3782,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
${photosPreviewHtml} ${photosPreviewHtml}
${post.text ? `<p class="archive-text">${escapeHtml(post.text.substring(0, 100))}${post.text.length > 100 ? '...' : ''}</p>` : ''} ${post.text ? `<p class="archive-text">${escapeHtml(post.text.substring(0, 100))}${post.text.length > 100 ? '...' : ''}</p>` : ''}
${warningText ? `<p class="archive-warning">⚠ ${escapeHtml(warningText)}</p>` : ''}
</div> </div>
`; `;
}).join(''); }).join('');