feat: resume recap feedback after interruptions (Closes #585)
This commit is contained in:
1 parent
c5ddb1275b
commit
3043ccc846
17 files changed
+148
-23
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user