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
|
|
@ -3840,8 +3840,18 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
d.setSeconds(0, 0);
|
||||
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) {
|
||||
const diff = (new Date(ts.replace(' ', 'T')) - Date.now()) / 1000;
|
||||
const diff = (utcToDate(ts) - Date.now()) / 1000;
|
||||
const abs = Math.abs(diff);
|
||||
const suf = diff > 0 ? '' : ' ago';
|
||||
const pre = diff > 0 ? 'in ' : '';
|
||||
|
|
@ -3851,7 +3861,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
return pre + Math.round(abs / 86400) + ' d' + suf;
|
||||
}
|
||||
function fmtDT(ts) {
|
||||
return ts.substring(0, 16).replace('T', ' ');
|
||||
return localDTStr(utcToDate(ts)).replace('T', ' ');
|
||||
}
|
||||
|
||||
// ── Emoji picker ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -4013,7 +4023,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
// ── Build edit pane HTML ──────────────────────────────────────────────────────
|
||||
function buildEditPane(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 imgPath = post?.image_path || '';
|
||||
const imgUrl = post?.image_url || '';
|
||||
|
|
@ -4076,21 +4086,21 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
async function doAutosave(card) {
|
||||
const post = card._post;
|
||||
if (!post) return;
|
||||
const content = card.querySelector('.sc-content').value.trim();
|
||||
const scheduledAt = card.querySelector('.sc-dt').value;
|
||||
const imagePath = card.querySelector('.sc-img-path').value;
|
||||
const targets = collectTargetsFrom(card.querySelector('.sc-targets'));
|
||||
const content = card.querySelector('.sc-content').value.trim();
|
||||
const scheduledAt = card.querySelector('.sc-dt').value;
|
||||
const imagePath = card.querySelector('.sc-img-path').value;
|
||||
const targets = collectTargetsFrom(card.querySelector('.sc-targets'));
|
||||
if (!content || !scheduledAt || !targets.length) return;
|
||||
|
||||
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 d = await r.json();
|
||||
if (d.error) { setSaveIndicator(card, 'error'); showCardError(card, d.error); }
|
||||
else {
|
||||
setSaveIndicator(card, 'saved');
|
||||
// keep in-memory post up to date so cancel reverts to latest saved state
|
||||
card._post = { ...post, content, scheduled_at: scheduledAt.replace('T', ' '), image_path: imagePath };
|
||||
card._post = { ...post, content, scheduled_at: scheduledUTC, image_path: imagePath };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4218,7 +4228,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
clearTimeout(card._autosaveTimer);
|
||||
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;
|
||||
|
||||
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