Add relative time to post cards; add Dispatch now button for manual testing

- View cards now show "2026-06-04 14:30 (in 2h)" next to the scheduled time
- New "Dispatch now" button runs dispatch-social.php immediately and shows
  output inline (useful for diagnosing why cron isn't sending posts)
- dispatch action added to /api/social

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bashy 2026-06-04 20:49:56 +03:00
parent e52f90445b
commit 629f2515e9
3 changed files with 31 additions and 1 deletions

View file

@ -12,8 +12,13 @@ include ROOT . '/views/_header.php';
<button class="btn btn-sm btn-outline-secondary" id="socialRefreshBtn" title="Refresh">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button class="btn btn-sm btn-outline-success" id="socialDispatchBtn" title="Run dispatcher now">
<i class="bi bi-send me-1"></i>Dispatch now
</button>
</div>
<pre id="socialDispatchOut" class="small bg-body-secondary border rounded p-2 mb-3 d-none" style="max-height:200px;overflow-y:auto;white-space:pre-wrap"></pre>
<div id="socialPage">
<div id="socialPostsGrid" class="row g-3">
<div class="col-12 text-center text-muted py-5 small">Loading…</div>

View file

@ -192,5 +192,15 @@ if ($action === 'retry') {
exit;
}
// ── DISPATCH NOW ──────────────────────────────────────────────────────────────
if ($action === 'dispatch') {
$script = ROOT . '/bin/dispatch-social.php';
$output = [];
$code = 0;
exec('/usr/bin/php ' . escapeshellarg($script) . ' 2>&1', $output, $code);
echo json_encode(['ok' => true, 'output' => implode("\n", $output), 'exit' => $code]);
exit;
}
http_response_code(400);
echo json_encode(['error' => 'Unknown action']);

View file

@ -3994,7 +3994,7 @@ window.addEventListener('DOMContentLoaded', () => {
return `<div class="sc-view-pane p-3 d-flex flex-column h-100">
<div class="d-flex align-items-start gap-2 mb-2">
<div class="flex-grow-1 min-w-0">
<div class="small text-muted">${esc(fmtDT(post.scheduled_at))}</div>
<div class="small text-muted">${esc(fmtDT(post.scheduled_at))} <span class="opacity-75">(${relTime(post.scheduled_at)})</span></div>
<div>${STATUS_BADGE(post.status)}</div>
</div>
<div class="d-flex align-items-center gap-1 flex-shrink-0">
@ -4308,6 +4308,21 @@ window.addEventListener('DOMContentLoaded', () => {
});
document.getElementById('socialRefreshBtn').addEventListener('click', loadPosts);
document.getElementById('socialDispatchBtn').addEventListener('click', async function () {
this.disabled = true;
this.innerHTML = '<i class="bi bi-hourglass-split me-1"></i>Running…';
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'dispatch' }) });
const d = await r.json();
this.disabled = false;
this.innerHTML = '<i class="bi bi-send me-1"></i>Dispatch now';
const out = d.output?.trim();
const msg = out || 'No pending posts were due (or all sent silently).';
const box = document.getElementById('socialDispatchOut');
box.textContent = msg;
box.classList.remove('d-none');
loadPosts();
});
// ── Per-card error helpers ────────────────────────────────────────────────────
function showCardError(card, msg) {
const el = card.querySelector('.sc-error');