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:
parent
30b21b6214
commit
45b9e15152
5 changed files with 66 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -53,9 +53,17 @@
|
|||
<div class="small fw-semibold" id="liConnectionLabel">Checking…</div>
|
||||
<div class="text-muted small" id="liTokenExpiry"></div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary ms-auto" id="liConnectBtn">
|
||||
<i class="bi bi-box-arrow-in-right me-1"></i>Connect LinkedIn
|
||||
</button>
|
||||
<div class="ms-auto d-flex gap-2 align-items-center">
|
||||
<button class="btn btn-sm btn-outline-primary" id="liConnectBtn">
|
||||
<i class="bi bi-box-arrow-in-right me-1"></i>Connect LinkedIn
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary d-none" id="liTestPostBtn" title="Test post to LinkedIn">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary d-none" id="liReconnectBtn" title="Reconnect OAuth">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,15 @@
|
|||
</div>
|
||||
|
||||
<!-- Post list -->
|
||||
<h6 class="mb-3">Scheduled & Recent Posts</h6>
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<h6 class="mb-0">Scheduled & Recent Posts</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-auto" id="schedRunNowBtn" title="Process all due posts now">
|
||||
<i class="bi bi-play-circle me-1"></i>Process Now
|
||||
</button>
|
||||
</div>
|
||||
<div id="schedRunNowOutput" class="d-none mb-2">
|
||||
<pre class="bg-body-secondary border rounded p-2 small mb-0" id="schedRunNowPre" style="max-height:160px;overflow:auto"></pre>
|
||||
</div>
|
||||
<div id="schedPostsList"><div class="text-muted small">Loading…</div></div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue