diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index af7c946..9e1800d 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -241,6 +241,25 @@ textarea { resize: vertical; min-height: 120px; }
.plan-today-header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.plan-today-header h2, .plan-today-header p { margin-top:0; }
.plan-today-header button { min-width:44px; min-height:44px; }
+.tomorrow-conflict-review { margin-top:14px; }
+.tomorrow-conflict-plans { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }
+.tomorrow-conflict-plans > section { min-width:0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
+.tomorrow-conflict-plans h4 { margin:0 0 8px; }
+.tomorrow-conflict-plan-summary { margin:0 0 8px; color:#bfdbfe; }
+.tomorrow-conflict-plan-list { margin:0; padding-left:22px; overflow-wrap:anywhere; }
+.tomorrow-conflict-plan-list li + li { margin-top:6px; }
+.tomorrow-conflict-actions { display:flex; gap:8px; margin-top:14px; }
+.tomorrow-conflict-actions button { min-height:44px; flex:1 1 0; }
+.tomorrow-conflict-status { min-height:1.4em; margin-top:8px; }
+.plan-today-sheet.tomorrow-conflict-mode .mobile-plan-today-nav,
+.plan-today-sheet.tomorrow-conflict-mode #plan-today-fit,
+.plan-today-sheet.tomorrow-conflict-mode #plan-today-selected,
+.plan-today-sheet.tomorrow-conflict-mode #plan-today-available-work,
+.plan-today-sheet.tomorrow-conflict-mode .plan-today-actions { display:none; }
+@media (max-width:480px) {
+ .tomorrow-conflict-plans { grid-template-columns:1fr; }
+ .tomorrow-conflict-actions { flex-direction:column; }
+}
.mobile-plan-today-nav { position:sticky; top:0; z-index:5; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:4px; margin:0 -18px 10px; padding:4px 18px; background:rgba(11,21,38,.98); border-block:1px solid #2a496e; }
.mobile-plan-today-nav button { min-width:0; min-height:44px; padding:4px; border-color:transparent; font-size:12px; }
.mobile-plan-today-nav button[aria-current="location"] { color:#bfdbfe; background:#17365a; border-color:#31577f; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 90c5712..198d6e4 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -400,15 +400,19 @@
return saved;
}).catch(error => {
const conflict = tomorrowPlan.conflict();
+ if (conflict) renderTomorrowQueueSummary({ids:[],sync_pending:false,conflict:true});
+ qs('#mobile-tomorrow-summary').textContent = conflict ? 'Conflict · review required' : qs('#mobile-tomorrow-summary').textContent;
qs('#my-work-action-status').textContent = conflict ?
- 'Another device changed Tomorrow. Your phone plan is preserved; reopen Plan Tomorrow to review it.' :
+ 'Another device changed Tomorrow. Both plans are preserved; open Tomorrow to choose one.' :
`${error.message || 'Tomorrow sync is unavailable.'} Saved on this phone · sync pending.`;
return false;
});
}
async function refreshTomorrowQueueSummary() {
try {
- renderTomorrowQueueSummary(await tomorrowPlan.load());
+ const loaded = await tomorrowPlan.load();
+ if (tomorrowPlan.conflict()) qs('#mobile-tomorrow-summary').textContent = 'Conflict · review required';
+ else renderTomorrowQueueSummary(loaded);
return true;
} catch (_error) {
qs('#mobile-tomorrow-summary').textContent = 'Unavailable · tap to retry';
@@ -2698,6 +2702,8 @@
}
planToday.cancel();
planningTomorrow = false;
+ qs('#tomorrow-conflict-review').hidden = true;
+ qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
qs('#plan-today-sheet').hidden = true;
document.body.classList.remove('task-overlay-open');
if (planTodayTrigger?.dataset.mobileQueue === 'tomorrow') {
@@ -3024,6 +3030,38 @@
});
let pendingPlanActualMinutes = null;
+ function tomorrowConflictPlanMarkup(value) {
+ const ids = value?.ids || [];
+ const estimates = value?.estimates || {};
+ const estimated = ids.reduce((total, id) => total + (Number(estimates[id]) || 0), 0);
+ const capacity = Number(value?.capacity_minutes) || 0;
+ const summary = ids.length ? `${ids.length} planned` +
+ (estimated && capacity ? ` · ${estimated} of ${capacity} min` : '') : 'Nothing planned';
+ const available = [...todayMyWork, ...activeMyWork];
+ const items = ids.map(id => {
+ const item = available.find(candidate => todayWork.identity(candidate) === id);
+ const label = item?.title || item?.key || id;
+ const estimate = Number(estimates[id]) || 0;
+ return `
${escapeHtml(label)}${estimate ? ` · ${estimate} min` : ''}`;
+ }).join('');
+ return `${summary}
` +
+ (items ? `${items}
` : 'No work selected.
');
+ }
+
+ function showTomorrowConflict(conflict) {
+ qs('#plan-today-title').textContent = 'Resolve Tomorrow conflict';
+ qs('#tomorrow-conflict-phone-plan').innerHTML = tomorrowConflictPlanMarkup(conflict.local);
+ qs('#tomorrow-conflict-server-plan').innerHTML = tomorrowConflictPlanMarkup(conflict.remote);
+ qs('#tomorrow-conflict-status').textContent = '';
+ qs('#tomorrow-conflict-review').hidden = false;
+ qs('#plan-today-sheet').classList.add('tomorrow-conflict-mode');
+ qs('#plan-today-sheet').hidden = false;
+ document.body.classList.add('task-overlay-open');
+ const keep = qs('#keep-phone-tomorrow');
+ keep.focus();
+ requestAnimationFrame(() => keep.focus());
+ }
+
function openPlanToday(trigger, navigate = true, actualMinutes = null) {
if (!planningOwnerLogin) {
qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
@@ -3037,6 +3075,13 @@
taskOverlayHistory.open('plan-today');
return;
}
+ const conflict = planningTomorrow ? tomorrowPlan.conflict() : null;
+ if (conflict) {
+ showTomorrowConflict(conflict);
+ return;
+ }
+ qs('#tomorrow-conflict-review').hidden = true;
+ qs('#plan-today-sheet').classList.remove('tomorrow-conflict-mode');
const recommendations = actualMinutes || pendingPlanActualMinutes || todayRecapView.pendingReplan()?.actual_minutes;
pendingPlanActualMinutes = null;
const protectProposal = pendingProtectToday;
@@ -7758,6 +7803,35 @@
qs('#refresh').addEventListener('click', load);
qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget));
qs('#plan-tomorrow').addEventListener('click', event => openTomorrowPlanner(event.currentTarget));
+ qs('#keep-phone-tomorrow').addEventListener('click', async () => {
+ const keep = qs('#keep-phone-tomorrow');
+ const use = qs('#use-server-tomorrow');
+ keep.disabled = true;
+ use.disabled = true;
+ qs('#tomorrow-conflict-status').textContent = 'Saving this phone’s plan…';
+ try {
+ const saved = await tomorrowPlan.keepLocal();
+ renderTomorrowQueueSummary(saved);
+ qs('#my-work-action-status').textContent = 'This phone’s Tomorrow plan is saved to your account. Today was not changed.';
+ closePlanToday();
+ } catch (error) {
+ const conflict = tomorrowPlan.conflict();
+ if (conflict) showTomorrowConflict(conflict);
+ qs('#tomorrow-conflict-status').textContent = error?.status === 409 ?
+ 'Tomorrow changed again. Both latest plans are still preserved; choose again.' :
+ `${error.message || 'Tomorrow could not be saved.'} Both plans are still preserved.`;
+ } finally {
+ keep.disabled = false;
+ use.disabled = false;
+ }
+ });
+ qs('#use-server-tomorrow').addEventListener('click', () => {
+ const adopted = tomorrowPlan.useRemote();
+ if (!adopted) return;
+ renderTomorrowQueueSummary(adopted);
+ qs('#my-work-action-status').textContent = 'Saved account Tomorrow plan selected. Today was not changed.';
+ closePlanToday();
+ });
qs('#cancel-plan-today').addEventListener('click', closePlanToday);
qs('#plan-today-sheet').addEventListener('click', event => {
if (event.target === qs('#plan-today-sheet')) closePlanToday();
diff --git a/frontend/index.html b/frontend/index.html
index 3908fca..4f5b6a3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -449,6 +449,26 @@
Plan Today
Choose and order the work you want to finish next.
+
+ Cross-device change
+ Choose which Tomorrow plan to keep
+ Both plans are preserved until one choice succeeds. Today will not change.
+
+
+
+ Saved on your account
+
+
+
+
+
+
+
+
+