feat: resume recap feedback after interruptions (Closes #585)
CI / lint (pull_request) Successful in 1m24s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 18:57:11 +00:00
1 parent c5ddb1275b
commit 3043ccc846
17 files changed
+148 -23

No files matched your search

+1
View File
@@ -127,6 +127,7 @@ textarea { resize: vertical; min-height: 120px; }
.plan-today-available input, .plan-today-estimate { box-sizing:border-box; min-height:44px; width:108px; }
.plan-today-estimate-wrap { display:flex; align-items:center; gap:6px; margin-top:8px; }
.plan-today-error { min-height:1.4em; color:#fca5a5; }
.discard-recap-replan { min-height:44px; margin-bottom:8px; }
.plan-today-list, .plan-today-candidates { display:grid; gap:8px; }
.plan-today-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:10px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; }
.plan-today-item-copy { min-width:0; overflow-wrap:anywhere; }
+11 -1
View File
@@ -1375,6 +1375,7 @@
if (capacityAware && !todaySync.enqueueConfiguration(plan.capacity_minutes, plan.estimates)) return false;
if (!todayWork.replace(ids)) return false;
if (capacityAware && !todayWork.replacePlanning(plan)) return false;
todayRecapView.completeReplan();
refreshMyWorkView();
todaySync.flush();
warmTodayOffline();
@@ -1640,9 +1641,10 @@
taskOverlayHistory.open('plan-today');
return;
}
const recommendations = actualMinutes || pendingPlanActualMinutes;
const recommendations = actualMinutes || pendingPlanActualMinutes || todayRecapView.pendingReplan()?.actual_minutes;
pendingPlanActualMinutes = null;
planToday.open(todayMyWork, activeMyWork, todayWork.planning(), recommendations);
qs('#discard-recap-replan').hidden = !todayRecapView.pendingReplan();
qs('#plan-today-error').textContent = '';
qs('#plan-today-sheet').hidden = false;
document.body.classList.add('task-overlay-open');
@@ -1650,6 +1652,14 @@
qs('#cancel-plan-today').focus();
}
qs('#discard-recap-replan').addEventListener('click', () => {
todayRecapView.discardReplan();
planToday.open(todayMyWork, activeMyWork, todayWork.planning());
qs('#discard-recap-replan').hidden = true;
qs('#plan-today-error').textContent = 'Recap feedback discarded. Your saved recap is unchanged.';
renderPlanToday();
});
const detailDefer = createDetailDefer({
laterWork,
session: workSession,
+1
View File
@@ -251,6 +251,7 @@
<span><input id="plan-today-available" type="number" inputmode="numeric" min="15" max="1440" step="15" placeholder="Minutes" /> min</span>
</label>
<div class="small plan-today-error" id="plan-today-error" role="alert"></div>
<button class="discard-recap-replan" id="discard-recap-replan" type="button" hidden>Discard recap feedback</button>
<section aria-labelledby="today-plan-heading">
<h3 id="today-plan-heading">Today, in order</h3>
<div class="plan-today-list" id="plan-today-list"></div>
+1 -1
View File
@@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v88';
const CACHE = 'stackchain-dashboard-shell-v89';
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
+43 -1
View File
@@ -23,6 +23,10 @@ function createTodayRecap({ save, clear, makeId = () => crypto.randomUUID(), sto
const login = String(getLogin() || '').trim().toLowerCase();
return login ? 'stackchain.today-recap-draft.v1.' + login : '';
};
const handoffKey = () => {
const login = String(getLogin() || '').trim().toLowerCase();
return login ? 'stackchain.today-recap-handoff.v1.' + login : '';
};
const validStoredDraft = saved => {
if (!saved || typeof saved.session_id !== 'string' || !saved.session_id.length || saved.session_id.length > 100 ||
!Array.isArray(saved.items) || !saved.items.length || saved.items.length > 20) return false;
@@ -47,6 +51,33 @@ function createTodayRecap({ save, clear, makeId = () => crypto.randomUUID(), sto
return null;
}
};
const validHandoff = saved => {
if (!saved || typeof saved.session_id !== 'string' || !saved.session_id.length || saved.session_id.length > 100 ||
!saved.actual_minutes || typeof saved.actual_minutes !== 'object' || Array.isArray(saved.actual_minutes)) return false;
const entries = Object.entries(saved.actual_minutes);
return entries.length > 0 && entries.length <= 20 && entries.every(([identity, minutes]) =>
identity.length > 0 && identity.length <= 500 && Number.isInteger(minutes) && minutes >= 0 && minutes <= 1440
);
};
const pendingReplan = () => {
const key = handoffKey();
if (!storage || !key) return null;
try {
const saved = JSON.parse(storage.getItem(key) || 'null');
if (!validHandoff(saved)) {
if (saved !== null) storage.removeItem(key);
return null;
}
return { session_id:saved.session_id, actual_minutes:{...saved.actual_minutes} };
} catch (_error) {
try { storage.removeItem(key); } catch (_ignored) {}
return null;
}
};
const completeReplan = () => {
const key = handoffKey();
if (storage && key) storage.removeItem(key);
};
let draftKey = storageKey();
let draft = load();
const persist = () => {
@@ -75,6 +106,9 @@ function createTodayRecap({ save, clear, makeId = () => crypto.randomUUID(), sto
payload.items.map(item => [item.identity, item.actual_minutes])
) : null;
const result = await save(payload);
if (includeActuals && storage && handoffKey()) storage.setItem(handoffKey(), JSON.stringify({
session_id:payload.session_id, actual_minutes:actualMinutes,
}));
clear();
if (storage && draftKey) storage.removeItem(draftKey);
draft = null;
@@ -106,6 +140,9 @@ function createTodayRecap({ save, clear, makeId = () => crypto.randomUUID(), sto
snapshot,
save:() => saveConfirmed(false),
saveForReplan:() => saveConfirmed(true),
pendingReplan,
completeReplan,
discardReplan:completeReplan,
};
}
@@ -193,7 +230,12 @@ function createTodayRecapView({ recap, timer, todayWork, api, fetchJson, qs, esc
qs('#discard-today-recap').addEventListener('click', close);
qs('#save-today-recap').addEventListener('click', event => saveDraft(event.currentTarget));
};
return { open, close, saveDraft, loadHistory, render, bind };
return {
open, close, saveDraft, loadHistory, render, bind,
pendingReplan:recap.pendingReplan,
completeReplan:recap.completeReplan,
discardReplan:recap.discardReplan,
};
}
function endTodaySession(workSession, qs) {