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:
Broque Thomas 2026-05-24 22:01:21 -07:00
parent 9a8e7d02a7
commit dc4d157944
4 changed files with 76 additions and 22 deletions

View file

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

View file

@ -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.' },

View file

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

View file

@ -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,