diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index f370e74..92ebf0e 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -768,6 +768,7 @@ textarea { resize: vertical; min-height: 120px; }
.new-issue { min-height:44px; }
.find-work-action { min-height:44px; }
.my-work-actions { display:flex; flex-wrap:wrap; gap:8px; }
+.plan-tomorrow { min-height:44px; }
.start-work-session { min-height:44px; }
.resume-today-session, .end-today-session { min-height:44px; }
.work-session-nav { position:sticky; bottom:0; z-index:5; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:12px; padding:10px 4px; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:rgba(11,21,38,.98); border-top:1px solid #2a496e; }
@@ -1095,6 +1096,7 @@ textarea { resize: vertical; min-height: 120px; }
.attention-interruption:not([hidden]) { position:sticky; top:64px; z-index:6; margin-inline:max(0px,env(safe-area-inset-left)) max(0px,env(safe-area-inset-right)); }
.my-work-header { align-items:flex-start; }
.my-work-actions { display:none; }
+ .my-work-actions { display:flex; }
.work-settings > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; padding:0 10px; border:1px solid #2a496e; border-radius:10px; font-weight:700; }
.work-settings:not([open]) > .work-settings-panel { display:none; }
.work-settings-panel { display:grid; gap:10px; margin-top:8px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 0fcac55..c093e59 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -376,10 +376,37 @@
let rolloverReviewPlan = null;
const outboxCoordinator = createOutboxCoordinator({ storage: localStorage });
const todayRollover = createTodayRollover();
+ let planningTomorrow = false;
+ const tomorrowPlan = createTomorrowPlan({
+ fetchJson:fetchReviewJson,
+ localDate:todayRollover.localDate,
+ timeZone:todayRollover.timeZone,
+ });
const todayWork = createTodayWork({
storage: localStorage,
getLogin: () => planningOwnerLogin,
});
+ async function promoteTomorrowIfDue(plan) {
+ try {
+ await tomorrowPlan.load();
+ const promoted = await tomorrowPlan.promote(plan.revision);
+ if (!promoted) return false;
+ todayWork.replace(promoted.ids);
+ todayWork.replacePlanning({
+ capacity_minutes:promoted.capacity_minutes ?? null,
+ estimates:promoted.estimates || {},
+ });
+ refreshMyWorkView();
+ warmTodayOffline();
+ qs('#my-work-action-status').textContent =
+ 'Your saved Tomorrow plan is now Today.';
+ return true;
+ } catch (error) {
+ qs('#my-work-action-status').textContent =
+ `${error.message || 'Tomorrow needs review before promotion.'} Open Plan Tomorrow to review.`;
+ return false;
+ }
+ }
const todaySync = createTodaySync({
storage: localStorage,
getLogin: () => planningOwnerLogin,
@@ -392,6 +419,7 @@
},
onRemotePlan: plan => {
if (!planningOwnerLogin) return;
+ promoteTomorrowIfDue(plan);
todayWork.replacePlanning({
capacity_minutes: plan.capacity_minutes ?? null,
estimates: plan.estimates || {},
@@ -2605,6 +2633,7 @@
return;
}
planToday.cancel();
+ planningTomorrow = false;
qs('#plan-today-sheet').hidden = true;
document.body.classList.remove('task-overlay-open');
planTodayTrigger?.focus();
@@ -2638,10 +2667,24 @@
return true;
}
+ function saveTomorrowPlan(plan) {
+ const normalized = Array.isArray(plan) ?
+ {ids:plan, capacity_minutes:null, estimates:{}} : plan;
+ tomorrowPlan.save(normalized).then(saved => {
+ qs('#my-work-action-status').textContent = saved.ids.length ?
+ `Tomorrow saved for ${saved.plan_date} without changing Today.` :
+ `Tomorrow cleared for ${saved.plan_date}.`;
+ }).catch(error => {
+ qs('#my-work-action-status').textContent =
+ `${error.message || 'Tomorrow could not be saved.'} Reopen Plan Tomorrow to retry.`;
+ });
+ return true;
+ }
+
const planToday = createPlanToday({
identity: item => todayWork.identity(item),
limit: todayWork.limit,
- save: saveTodayPlan,
+ save: plan => planningTomorrow ? saveTomorrowPlan(plan) : saveTodayPlan(plan),
start: () => {
qs('[data-work-filter="today"]').click();
startTodaySession();
@@ -2919,7 +2962,8 @@
return;
}
if (trigger) planTodayTrigger = trigger;
- qs('#plan-today-title').textContent = pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today');
+ qs('#plan-today-title').textContent = planningTomorrow ? 'Plan Tomorrow' :
+ (pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today'));
if (actualMinutes) pendingPlanActualMinutes = actualMinutes;
if (navigate) {
taskOverlayHistory.open('plan-today');
@@ -2928,8 +2972,18 @@
const recommendations = actualMinutes || pendingPlanActualMinutes || todayRecapView.pendingReplan()?.actual_minutes;
pendingPlanActualMinutes = null;
const protectProposal = pendingProtectToday;
- const selected = protectProposal?.selected || todayMyWork;
- planToday.open(selected, activeMyWork, todayWork.planning(), recommendations);
+ const tomorrow = planningTomorrow ? tomorrowPlan.state() : null;
+ const availablePlanningItems = [...todayMyWork, ...activeMyWork].filter((item, index, items) =>
+ items.findIndex(candidate => todayWork.identity(candidate) === todayWork.identity(item)) === index
+ );
+ const selected = planningTomorrow ? availablePlanningItems.filter(item =>
+ tomorrow.ids.includes(todayWork.identity(item))
+ ) : (protectProposal?.selected || todayMyWork);
+ planToday.open(selected, activeMyWork, planningTomorrow ? tomorrow : todayWork.planning(), recommendations);
+ qs('#today-plan-heading').textContent = planningTomorrow ? 'Tomorrow, in order' : 'Today, in order';
+ qs('.plan-today-available').firstChild.textContent = planningTomorrow ? 'Available tomorrow' : 'Available today';
+ qs('#build-today-plan').textContent = planningTomorrow ? 'Build my Tomorrow' : 'Build my Today';
+ qs('#save-and-start-today').hidden = planningTomorrow;
pendingProtectToday = null;
qs('#discard-recap-replan').hidden = !todayRecapView.pendingReplan();
qs('#plan-today-error').textContent = '';
@@ -7633,6 +7687,25 @@
qs('#refresh').addEventListener('click', load);
qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget));
+ qs('#plan-tomorrow').addEventListener('click', async event => {
+ const button = event.currentTarget;
+ if (!planningOwnerLogin) {
+ qs('#my-work-action-status').textContent = 'Planning is unavailable until your operator identity is restored.';
+ return;
+ }
+ button.disabled = true;
+ qs('#my-work-action-status').textContent = 'Loading Tomorrow…';
+ try {
+ await tomorrowPlan.load();
+ planningTomorrow = true;
+ openPlanToday(button);
+ qs('#my-work-action-status').textContent = '';
+ } catch (error) {
+ qs('#my-work-action-status').textContent = `${error.message || 'Tomorrow is unavailable.'} Retry when connected.`;
+ } finally {
+ button.disabled = false;
+ }
+ });
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 85ba709..f595694 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -192,6 +192,7 @@
+