From 45b9e151529ede0e7bcc0957f55c012c2f55c71d Mon Sep 17 00:00:00 2001 From: Bashy Date: Wed, 3 Jun 2026 15:17:26 +0300 Subject: [PATCH] Improve LinkedIn UI and scheduling robustness - When LinkedIn is connected, replace the Connect button with a pencil (test post to first personal profile) and a reconnect arrow icon - Add Process Now button to the Schedule tab that shells out to the cron script on demand and shows its output inline - Add flock() to the cron script to prevent overlapping runs - Fix cron install path comment to match deployed location Co-Authored-By: Claude Sonnet 4.6 --- bin/process-scheduled-posts.php | 8 ++++++-- views/project/_tab_botconfig.php | 14 ++++++++++--- views/project/_tab_botschedule.php | 10 +++++++++- web/api/post_schedule.php | 9 +++++++++ web/assets/js/app.js | 32 +++++++++++++++++++++++++++++- 5 files changed, 66 insertions(+), 7 deletions(-) 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
+ +
+
+

+  
Loading…
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(); })();