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 <noreply@anthropic.com>
This commit is contained in:
Bashy 2026-06-03 15:17:26 +03:00
parent 30b21b6214
commit 45b9e15152
5 changed files with 66 additions and 7 deletions

View file

@ -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);

View file

@ -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();
})();