Loading…
diff --git a/web/api/social.php b/web/api/social.php
index 0210a0a..1b08762 100644
--- a/web/api/social.php
+++ b/web/api/social.php
@@ -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']);
diff --git a/web/assets/js/app.js b/web/assets/js/app.js
index 59a6981..242151f 100644
--- a/web/assets/js/app.js
+++ b/web/assets/js/app.js
@@ -3994,7 +3994,7 @@ window.addEventListener('DOMContentLoaded', () => {
return `
-
${esc(fmtDT(post.scheduled_at))}
+
${esc(fmtDT(post.scheduled_at))} (${relTime(post.scheduled_at)})
${STATUS_BADGE(post.status)}
@@ -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 = '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 = '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');