Fix timezone mismatch: store/compare scheduled_at in UTC
JS was sending browser-local time; dispatcher compared against server UTC, so posts scheduled in UTC+3 appeared 3h in the future to the dispatcher. - JS helpers: utcToDate, utcToLocalInput, localInputToUTC - All scheduled_at values sent to server are now converted to UTC - Stored values are displayed converted back to browser local time - relTime and fmtDT parse stored UTC timestamps correctly - dispatch-social.php: gmdate() for $now (UTC) + sent_at Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9af6326be2
commit
30242ae470
2 changed files with 22 additions and 12 deletions
|
|
@ -14,7 +14,7 @@ require ROOT . '/lib/bootstrap.php';
|
||||||
$lock = fopen(sys_get_temp_dir() . '/hackman_social.lock', 'c');
|
$lock = fopen(sys_get_temp_dir() . '/hackman_social.lock', 'c');
|
||||||
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) exit(0);
|
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) exit(0);
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT t.id AS target_id, t.post_id, t.project_id, t.platform, t.target_key, t.include_image,
|
SELECT t.id AS target_id, t.post_id, t.project_id, t.platform, t.target_key, t.include_image,
|
||||||
|
|
|
||||||
|
|
@ -3840,8 +3840,18 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
d.setSeconds(0, 0);
|
d.setSeconds(0, 0);
|
||||||
return localDTStr(d);
|
return localDTStr(d);
|
||||||
}
|
}
|
||||||
|
// ts is stored UTC ("2026-06-04 17:51:00") — parse with Z so JS treats it as UTC
|
||||||
|
function utcToDate(ts) { return new Date(ts.replace(' ', 'T') + 'Z'); }
|
||||||
|
// Convert stored UTC string → "YYYY-MM-DDTHH:MM" in browser local time (for datetime-local input)
|
||||||
|
function utcToLocalInput(ts) { return localDTStr(utcToDate(ts)); }
|
||||||
|
// Convert datetime-local value (local time) → "YYYY-MM-DD HH:MM:00" UTC for the server
|
||||||
|
function localInputToUTC(dtLocal) {
|
||||||
|
const d = new Date(dtLocal); // browser parses datetime-local as local time
|
||||||
|
const p = n => String(n).padStart(2, '0');
|
||||||
|
return `${d.getUTCFullYear()}-${p(d.getUTCMonth()+1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:00`;
|
||||||
|
}
|
||||||
function relTime(ts) {
|
function relTime(ts) {
|
||||||
const diff = (new Date(ts.replace(' ', 'T')) - Date.now()) / 1000;
|
const diff = (utcToDate(ts) - Date.now()) / 1000;
|
||||||
const abs = Math.abs(diff);
|
const abs = Math.abs(diff);
|
||||||
const suf = diff > 0 ? '' : ' ago';
|
const suf = diff > 0 ? '' : ' ago';
|
||||||
const pre = diff > 0 ? 'in ' : '';
|
const pre = diff > 0 ? 'in ' : '';
|
||||||
|
|
@ -3851,7 +3861,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
return pre + Math.round(abs / 86400) + ' d' + suf;
|
return pre + Math.round(abs / 86400) + ' d' + suf;
|
||||||
}
|
}
|
||||||
function fmtDT(ts) {
|
function fmtDT(ts) {
|
||||||
return ts.substring(0, 16).replace('T', ' ');
|
return localDTStr(utcToDate(ts)).replace('T', ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Emoji picker ─────────────────────────────────────────────────────────────
|
// ── Emoji picker ─────────────────────────────────────────────────────────────
|
||||||
|
|
@ -4013,7 +4023,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
// ── Build edit pane HTML ──────────────────────────────────────────────────────
|
// ── Build edit pane HTML ──────────────────────────────────────────────────────
|
||||||
function buildEditPane(post) {
|
function buildEditPane(post) {
|
||||||
const isNew = !post;
|
const isNew = !post;
|
||||||
const dt = post ? post.scheduled_at.replace(' ', 'T').substring(0, 16) : defaultScheduleTime();
|
const dt = post ? utcToLocalInput(post.scheduled_at) : defaultScheduleTime();
|
||||||
const content = post?.content || '';
|
const content = post?.content || '';
|
||||||
const imgPath = post?.image_path || '';
|
const imgPath = post?.image_path || '';
|
||||||
const imgUrl = post?.image_url || '';
|
const imgUrl = post?.image_url || '';
|
||||||
|
|
@ -4076,21 +4086,21 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
async function doAutosave(card) {
|
async function doAutosave(card) {
|
||||||
const post = card._post;
|
const post = card._post;
|
||||||
if (!post) return;
|
if (!post) return;
|
||||||
const content = card.querySelector('.sc-content').value.trim();
|
const content = card.querySelector('.sc-content').value.trim();
|
||||||
const scheduledAt = card.querySelector('.sc-dt').value;
|
const scheduledAt = card.querySelector('.sc-dt').value;
|
||||||
const imagePath = card.querySelector('.sc-img-path').value;
|
const imagePath = card.querySelector('.sc-img-path').value;
|
||||||
const targets = collectTargetsFrom(card.querySelector('.sc-targets'));
|
const targets = collectTargetsFrom(card.querySelector('.sc-targets'));
|
||||||
if (!content || !scheduledAt || !targets.length) return;
|
if (!content || !scheduledAt || !targets.length) return;
|
||||||
|
|
||||||
setSaveIndicator(card, 'saving');
|
setSaveIndicator(card, 'saving');
|
||||||
const body = { action: 'update', id: post.id, content, scheduled_at: scheduledAt, image_path: imagePath, targets };
|
const scheduledUTC = localInputToUTC(scheduledAt);
|
||||||
|
const body = { action: 'update', id: post.id, content, scheduled_at: scheduledUTC, image_path: imagePath, targets };
|
||||||
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (d.error) { setSaveIndicator(card, 'error'); showCardError(card, d.error); }
|
if (d.error) { setSaveIndicator(card, 'error'); showCardError(card, d.error); }
|
||||||
else {
|
else {
|
||||||
setSaveIndicator(card, 'saved');
|
setSaveIndicator(card, 'saved');
|
||||||
// keep in-memory post up to date so cancel reverts to latest saved state
|
card._post = { ...post, content, scheduled_at: scheduledUTC, image_path: imagePath };
|
||||||
card._post = { ...post, content, scheduled_at: scheduledAt.replace('T', ' '), image_path: imagePath };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4218,7 +4228,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
clearTimeout(card._autosaveTimer);
|
clearTimeout(card._autosaveTimer);
|
||||||
setSaveIndicator(card, 'saving');
|
setSaveIndicator(card, 'saving');
|
||||||
|
|
||||||
const body = { action: isNew ? 'create' : 'update', content, scheduled_at: scheduledAt, image_path: imagePath, targets };
|
const body = { action: isNew ? 'create' : 'update', content, scheduled_at: localInputToUTC(scheduledAt), image_path: imagePath, targets };
|
||||||
if (!isNew) body.id = post.id;
|
if (!isNew) body.id = post.id;
|
||||||
|
|
||||||
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue