diff --git a/bin/process-scheduled-posts.php b/bin/process-scheduled-posts.php
index 5915611..0f534e5 100644
--- a/bin/process-scheduled-posts.php
+++ b/bin/process-scheduled-posts.php
@@ -2,13 +2,17 @@
/**
* Cron-driven scheduled social post processor.
*
- * Install once on the host (as the apache user):
- * * * * * * /usr/bin/php /opt/hackmancms/bin/process-scheduled-posts.php >> /var/log/hackman_scheduled_posts.log 2>&1
+ * Install once on the host (as root, in /etc/cron.d/hackmancms):
+ * * * * * * www-data /usr/bin/php /var/www/html/hackmancms/bin/process-scheduled-posts.php >> /var/log/hackman_scheduled_posts.log 2>&1
*/
define('ROOT', dirname(__DIR__));
require ROOT . '/lib/bootstrap.php';
+// Prevent overlapping runs
+$_lock_fp = fopen(sys_get_temp_dir() . '/hackman_schedule.lock', 'c');
+if (!$_lock_fp || !flock($_lock_fp, LOCK_EX | LOCK_NB)) exit(0);
+
$now = gmdate('Y-m-d H:i:s');
// Load all due pending targets grouped by post
diff --git a/views/project/_tab_botconfig.php b/views/project/_tab_botconfig.php
index 464020f..2385c14 100644
--- a/views/project/_tab_botconfig.php
+++ b/views/project/_tab_botconfig.php
@@ -53,9 +53,17 @@
Checking…
-
+
+
+
+
+
diff --git a/views/project/_tab_botschedule.php b/views/project/_tab_botschedule.php
index f72a86e..5a34b98 100644
--- a/views/project/_tab_botschedule.php
+++ b/views/project/_tab_botschedule.php
@@ -74,7 +74,15 @@
- Scheduled & Recent Posts
+
+
Scheduled & Recent Posts
+
+
+
diff --git a/web/api/post_schedule.php b/web/api/post_schedule.php
index 4bc372d..94c94b3 100644
--- a/web/api/post_schedule.php
+++ b/web/api/post_schedule.php
@@ -103,6 +103,15 @@ if ($action === 'delete') {
exit;
}
+// ── RUN NOW ───────────────────────────────────────────────────────────────────
+if ($action === 'run_now') {
+ $script = ROOT . '/bin/process-scheduled-posts.php';
+ if (!file_exists($script)) { echo json_encode(['error' => 'Cron script not found']); exit; }
+ $output = shell_exec('/usr/bin/php ' . escapeshellarg($script) . ' 2>&1');
+ echo json_encode(['ok' => true, 'output' => $output ?? '(no output)']);
+ exit;
+}
+
// ── RETRY FAILED TARGET ───────────────────────────────────────────────────────
if ($action === 'retry_target') {
$target_id = (int)($input['target_id'] ?? 0);
diff --git a/web/assets/js/app.js b/web/assets/js/app.js
index 3180646..54545d4 100644
--- a/web/assets/js/app.js
+++ b/web/assets/js/app.js
@@ -3469,13 +3469,25 @@ window.addEventListener('DOMContentLoaded', () => {
label.textContent = connected ? 'Connected to LinkedIn' : 'Not connected';
label.className = 'small fw-semibold ' + (connected ? 'text-success' : 'text-muted');
expiry.textContent = li.token_expiry ? 'Expires: ' + li.token_expiry : '';
+ document.getElementById('liConnectBtn').classList.toggle('d-none', connected);
+ document.getElementById('liTestPostBtn').classList.toggle('d-none', !connected);
+ document.getElementById('liReconnectBtn').classList.toggle('d-none', !connected);
}
- document.getElementById('liConnectBtn').addEventListener('click', async () => {
+ async function _liOAuthRedirect() {
const r = await fetch('/api/linkedin_auth?project_id=' + pid);
const d = await r.json();
if (d.error) { showError(d.error); return; }
window.location.href = d.url;
+ }
+ document.getElementById('liConnectBtn').addEventListener('click', _liOAuthRedirect);
+ document.getElementById('liReconnectBtn').addEventListener('click', _liOAuthRedirect);
+
+ document.getElementById('liTestPostBtn').addEventListener('click', () => {
+ const pages = _config?.linkedin?.pages || {};
+ const personalKey = Object.keys(pages).find(k => pages[k].type === 'personal');
+ if (!personalKey) { showError('No personal LinkedIn profile configured — add one under LinkedIn Pages.'); return; }
+ openComposeModal('linkedin', personalKey, pages[personalKey].label || personalKey);
});
function renderLinkedinPages(pages) {
@@ -3749,6 +3761,24 @@ window.addEventListener('DOMContentLoaded', () => {
}));
}
+ document.getElementById('schedRunNowBtn')?.addEventListener('click', async () => {
+ const btn = document.getElementById('schedRunNowBtn');
+ const outBox = document.getElementById('schedRunNowOutput');
+ const pre = document.getElementById('schedRunNowPre');
+ btn.disabled = true;
+ outBox.classList.add('d-none');
+ const r = await fetch('/api/post_schedule', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ project_id: +pid, action: 'run_now' }),
+ });
+ const d = await r.json();
+ btn.disabled = false;
+ if (d.error) { showError(d.error); return; }
+ pre.textContent = d.output || '(no output — nothing due)';
+ outBox.classList.remove('d-none');
+ loadPosts();
+ });
+
loadTargets();
loadPosts();
})();