Fix Auto-Sync next-run countdown and theme its modal
The Playlist Auto-Sync schedule board was showing "next in 8h" on every
card regardless of the configured interval. Root cause: backend stores
next_run as a naive UTC string ("2026-05-25 05:00:00") and the new
auto-sync renderer was parsing it with plain `new Date(...)`, which
treats unmarked timestamps as local time. On Pacific time that offsets
the displayed countdown by ~8 hours. Auto-Sync now routes through the
existing `_autoParseUTC` helper that the rest of the Automations page
already uses, so countdowns line up with the wall clock.
A separate correctness fix in the automation update API: when a PUT
changes `trigger_type` or `trigger_config`, the stored `next_run` is
now blanked before the engine reschedules. Previously the scheduler's
restart-survival path would preserve a stale future timestamp from the
prior interval, so dragging a playlist from the 8h column to the 1h
column kept firing at the old 8h mark. Boot-time restart behavior is
unchanged — only user-driven schedule changes reset the clock.
Modal restyle: the Auto-Sync manager's hardcoded sky-blue palette is
replaced with `var(--accent-rgb)` everywhere so the modal honors the
user's chosen accent color. Tinted glow on the modal border, tabbed
header active state, scheduled-playlist chips, scrollbars, and a new
drag-over highlight on columns all follow the accent theme. The
column drag-over state is wired through new ondragleave handling so
the highlight clears reliably when leaving a column.
This commit is contained in:
parent
9a8e7d02a7
commit
dc4d157944
4 changed files with 76 additions and 22 deletions
|
|
@ -225,6 +225,13 @@ def update_automation(
|
|||
if cycle_path:
|
||||
return {'error': f'Signal cycle detected: {cycle_path}. This would cause an infinite loop.'}, 400
|
||||
|
||||
trigger_changed = (
|
||||
'trigger_type' in update_fields
|
||||
or 'trigger_config' in update_fields
|
||||
)
|
||||
if trigger_changed:
|
||||
update_fields['next_run'] = None
|
||||
|
||||
success = database.update_automation(automation_id, **update_fields)
|
||||
if not success:
|
||||
return {'error': 'Automation not found'}, 404
|
||||
|
|
|
|||
|
|
@ -3413,6 +3413,11 @@ function closeHelperSearch() {
|
|||
// projects that span multiple commits before shipping. Strip the flag at
|
||||
// release time and add a real `date:` line at the top of the version block.
|
||||
const WHATS_NEW = {
|
||||
'2.6.2': [
|
||||
{ date: 'May 24, 2026 — 2.6.2 release' },
|
||||
{ title: 'Fix: Auto-Sync "next in 8h" timezone bug', desc: 'scheduled Auto-Sync playlists were all showing "next in 8h" regardless of the interval — Every 1 hour, Every 1 day, anything. backend stores next-run as a naive UTC string and the frontend was parsing it as local time, which on Pacific time offset the displayed countdown by ~8 hours. Auto-Sync now uses the existing UTC-aware parser the rest of the Automations page already uses. as a separate correctness fix, the automation update endpoint also now blanks the stored next-run whenever the trigger type or trigger config changes, so the engine recomputes from scratch instead of preserving a leftover timestamp from the previous schedule.' },
|
||||
{ title: 'Auto-Sync modal restyled to your accent color', desc: 'the Playlist Auto-Sync manager now picks up your chosen accent (the same one used everywhere else in the app) instead of the hardcoded sky-blue palette it shipped with. tabs, drop zones, scheduled-playlist chips, scrollbars, and the modal glow all follow the accent theme. drop targets also light up clearly while dragging.' },
|
||||
],
|
||||
'2.6.1': [
|
||||
{ date: 'May 24, 2026 — 2.6.1 release' },
|
||||
{ title: 'React Import page polish', desc: 'Import now runs through the React route stack with album, singles, and auto-import tabs plus the state fixes needed for reliable Vite builds.' },
|
||||
|
|
|
|||
|
|
@ -818,7 +818,7 @@ function renderAutoSyncSchedulePanel(playlists, playlistSchedules) {
|
|||
const bucketHtml = AUTO_SYNC_BUCKETS.map(hours => {
|
||||
const assigned = schedulablePlaylists.filter(p => playlistSchedules[p.id]?.hours === hours);
|
||||
return `
|
||||
<div class="auto-sync-column" data-hours="${hours}" ondragover="autoSyncDragOver(event)" ondrop="autoSyncDrop(event, ${hours})">
|
||||
<div class="auto-sync-column" data-hours="${hours}" ondragover="autoSyncDragOver(event)" ondragleave="autoSyncDragLeave(event)" ondrop="autoSyncDrop(event, ${hours})">
|
||||
<div class="auto-sync-column-head">
|
||||
<span>${autoSyncBucketLabel(hours)}</span>
|
||||
<small>${assigned.length} playlist${assigned.length === 1 ? '' : 's'}</small>
|
||||
|
|
@ -911,7 +911,7 @@ function autoSyncScheduledCardHtml(playlist, schedule) {
|
|||
|
||||
function autoSyncNextRunLabel(nextRun) {
|
||||
if (!nextRun) return '';
|
||||
const ts = new Date(nextRun).getTime();
|
||||
const ts = _autoParseUTC(nextRun);
|
||||
if (!Number.isFinite(ts)) return '';
|
||||
const diff = ts - Date.now();
|
||||
if (diff <= 0) return 'due now';
|
||||
|
|
@ -932,10 +932,23 @@ function autoSyncDragStart(event) {
|
|||
function autoSyncDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
const col = event.currentTarget;
|
||||
if (col && !col.classList.contains('drag-over')) {
|
||||
col.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
function autoSyncDragLeave(event) {
|
||||
const col = event.currentTarget;
|
||||
if (!col) return;
|
||||
if (col.contains(event.relatedTarget)) return;
|
||||
col.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
async function autoSyncDrop(event, hours) {
|
||||
event.preventDefault();
|
||||
const col = event.currentTarget;
|
||||
if (col) col.classList.remove('drag-over');
|
||||
const playlistId = parseInt(event.dataTransfer.getData('text/plain'), 10);
|
||||
if (!playlistId) return;
|
||||
await saveAutoSyncPlaylistSchedule(playlistId, hours);
|
||||
|
|
|
|||
|
|
@ -11179,9 +11179,15 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
.sync-history-btn:hover { color: rgb(var(--accent-rgb)); background: rgba(var(--accent-rgb), 0.1); border-color: rgba(var(--accent-rgb), 0.3); }
|
||||
|
||||
.auto-sync-manager-btn {
|
||||
color: #7dd3fc;
|
||||
border-color: rgba(56, 189, 248, 0.28);
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
border-color: rgba(var(--accent-rgb), 0.32);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.auto-sync-manager-btn:hover {
|
||||
border-color: rgba(var(--accent-rgb), 0.5);
|
||||
background: rgba(var(--accent-rgb), 0.2);
|
||||
color: rgb(var(--accent-neon-rgb));
|
||||
}
|
||||
|
||||
.auto-sync-overlay {
|
||||
|
|
@ -11198,10 +11204,14 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
.auto-sync-modal {
|
||||
width: min(1500px, calc(100vw - 40px));
|
||||
height: min(860px, calc(100vh - 40px));
|
||||
background: rgba(17, 19, 27, 0.98);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.5);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(var(--accent-rgb), 0.08) 0%, transparent 60%),
|
||||
rgba(17, 19, 27, 0.98);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
0 28px 80px rgba(0, 0, 0, 0.5),
|
||||
0 0 60px rgba(var(--accent-rgb), 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
|
@ -11219,10 +11229,11 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
|
||||
.auto-sync-eyebrow {
|
||||
margin-bottom: 6px;
|
||||
color: #7dd3fc;
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.auto-sync-header h3 {
|
||||
|
|
@ -11308,9 +11319,10 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
|
||||
.auto-sync-tabs button:hover,
|
||||
.auto-sync-tabs button.active {
|
||||
border-color: rgba(56, 189, 248, 0.35);
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
color: #e0f2fe;
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
color: rgb(var(--accent-neon-rgb));
|
||||
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.08), 0 2px 10px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.auto-sync-tab-panel {
|
||||
|
|
@ -11331,8 +11343,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 18px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
background: rgba(56, 189, 248, 0.035);
|
||||
border-bottom: 1px solid rgba(var(--accent-rgb), 0.16);
|
||||
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.08) 0%, rgba(var(--accent-rgb), 0.03) 60%, transparent 100%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -11423,12 +11435,14 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
|
||||
.auto-sync-playlist:hover,
|
||||
.auto-sync-scheduled-card:hover {
|
||||
border-color: rgba(56, 189, 248, 0.32);
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
box-shadow: 0 4px 14px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.auto-sync-playlist.scheduled {
|
||||
border-color: rgba(34, 197, 94, 0.22);
|
||||
border-color: rgba(var(--accent-rgb), 0.3);
|
||||
background: rgba(var(--accent-rgb), 0.05);
|
||||
}
|
||||
|
||||
.auto-sync-playlist.unavailable {
|
||||
|
|
@ -11480,8 +11494,9 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
}
|
||||
|
||||
.auto-sync-scheduled-timing span {
|
||||
background: rgba(56, 189, 248, 0.14);
|
||||
color: #7dd3fc;
|
||||
background: rgba(var(--accent-rgb), 0.18);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.25);
|
||||
}
|
||||
|
||||
.auto-sync-scheduled-timing small {
|
||||
|
|
@ -11510,6 +11525,13 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
background: rgba(255, 255, 255, 0.025);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.auto-sync-column.drag-over {
|
||||
border-color: rgba(var(--accent-rgb), 0.6);
|
||||
background: rgba(var(--accent-rgb), 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.3), 0 6px 20px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
.auto-sync-column-head {
|
||||
|
|
@ -11524,7 +11546,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
}
|
||||
|
||||
.auto-sync-column-head small {
|
||||
color: #7dd3fc;
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -11548,10 +11570,17 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
.auto-sync-board::-webkit-scrollbar-thumb,
|
||||
.auto-sync-source-list::-webkit-scrollbar-thumb,
|
||||
.auto-sync-automation-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(125, 211, 252, 0.22);
|
||||
background: rgba(var(--accent-rgb), 0.3);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.auto-sync-column-list::-webkit-scrollbar-thumb:hover,
|
||||
.auto-sync-board::-webkit-scrollbar-thumb:hover,
|
||||
.auto-sync-source-list::-webkit-scrollbar-thumb:hover,
|
||||
.auto-sync-automation-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(var(--accent-rgb), 0.5);
|
||||
}
|
||||
|
||||
.auto-sync-drop-hint,
|
||||
.auto-sync-empty,
|
||||
.auto-sync-loading,
|
||||
|
|
|
|||
Loading…
Reference in a new issue