feat: replan Today from live budget risk (Closes #597)
This commit is contained in:
1 parent
798790ce1a
commit
ebe19f2281
15 files changed
+254
-32
No files matched your search
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v91';
|
||||
const CACHE = 'stackchain-dashboard-shell-v92';
|
||||
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;
|
||||
|
||||
+84
-4
@@ -188,13 +188,42 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||
clearRecap() {
|
||||
return write(empty());
|
||||
},
|
||||
totalElapsed() {
|
||||
const state = read();
|
||||
return Object.entries(state.entries).reduce((total, [identity, entry]) => {
|
||||
const live = identity === state.active_identity && entry.running ?
|
||||
Math.max(0, now() - Number(entry.started_at ?? now())) : 0;
|
||||
return total + Math.max(0, Number(entry.elapsed_ms) || 0) + live;
|
||||
}, 0);
|
||||
},
|
||||
snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate }) {
|
||||
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway }) {
|
||||
let progress = null;
|
||||
let runway = null;
|
||||
if (typeof document !== 'undefined') queryAll('.work-session-nav').forEach(nav => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.hidden = true;
|
||||
button.dataset.workSessionAdjustPlan = '';
|
||||
button.textContent = 'Adjust remaining plan';
|
||||
nav.insertBefore(button, nav.querySelector('[data-work-session-complete]'));
|
||||
});
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
const plan = queryAll('#plan-today')[0];
|
||||
const sheet = queryAll('#plan-today-sheet')[0];
|
||||
if (plan && sheet) {
|
||||
const replan = createTodayBudgetReplan({ timer, openPlan:() => plan.click() });
|
||||
queryAll('[data-work-session-adjust-plan]').forEach(button =>
|
||||
button.addEventListener('click', () => replan.open())
|
||||
);
|
||||
new MutationObserver(() => {
|
||||
if (sheet.hidden && replan.restore()) render();
|
||||
}).observe(sheet, { attributes:true, attributeFilter:['hidden'] });
|
||||
}
|
||||
}
|
||||
const elapsed = milliseconds => {
|
||||
const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000));
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
@@ -205,11 +234,37 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate }) {
|
||||
const render = () => {
|
||||
if (!progress) return;
|
||||
const snapshot = timer.snapshot();
|
||||
const sourceRunway = getRunway?.(snapshot) || runway;
|
||||
const liveRunway = sourceRunway?.future_minutes !== undefined ? (() => {
|
||||
const currentElapsed = Math.max(0, Math.ceil(snapshot.elapsed_ms / 60000));
|
||||
const currentRemaining = sourceRunway.current_minutes === null ? null :
|
||||
Math.max(0, sourceRunway.current_minutes - currentElapsed);
|
||||
const remaining = currentRemaining === null || sourceRunway.future_minutes === null ? null :
|
||||
currentRemaining + sourceRunway.future_minutes;
|
||||
const projected = remaining === null ? null : Math.ceil(timer.totalElapsed() / 60000) + remaining;
|
||||
const capacityRemaining = sourceRunway.capacity_minutes === null || projected === null ? null :
|
||||
sourceRunway.capacity_minutes - projected;
|
||||
return {
|
||||
...sourceRunway,
|
||||
remaining_minutes:remaining,
|
||||
over_estimate_minutes:sourceRunway.current_minutes === null ? 0 :
|
||||
Math.max(0, currentElapsed - sourceRunway.current_minutes),
|
||||
over_capacity_minutes:capacityRemaining === null ? 0 : Math.max(0, -capacityRemaining),
|
||||
};
|
||||
})() : sourceRunway;
|
||||
const timing = isActive() && snapshot.identity ? ' · ' + elapsed(snapshot.elapsed_ms) +
|
||||
(runway?.current_minutes ? ' / ' + formatEstimate(runway.current_minutes) : '') : '';
|
||||
(liveRunway?.current_minutes ? ' / ' + formatEstimate(liveRunway.current_minutes) : '') : '';
|
||||
const estimateRisk = liveRunway?.over_estimate_minutes ?
|
||||
' · ' + formatEstimate(liveRunway.over_estimate_minutes) + ' over estimate' : '';
|
||||
const capacityRisk = liveRunway?.over_capacity_minutes ?
|
||||
' · Today projected ' + formatEstimate(liveRunway.over_capacity_minutes) + ' over capacity' : '';
|
||||
queryAll('[data-work-session-progress]').forEach(element => {
|
||||
element.textContent = 'Item ' + progress.index + ' of ' + progress.total + timing +
|
||||
(runway?.remaining_minutes ? ' · ' + formatEstimate(runway.remaining_minutes) + ' remaining' : '');
|
||||
element.textContent = 'Item ' + progress.index + ' of ' + progress.total + timing + estimateRisk + capacityRisk +
|
||||
(!estimateRisk && !capacityRisk && liveRunway?.remaining_minutes ?
|
||||
' · ' + formatEstimate(liveRunway.remaining_minutes) + ' remaining' : '');
|
||||
});
|
||||
queryAll('[data-work-session-adjust-plan]').forEach(button => {
|
||||
button.hidden = !(liveRunway?.over_estimate_minutes || liveRunway?.over_capacity_minutes);
|
||||
});
|
||||
queryAll('[data-work-session-timer-toggle]').forEach(button => {
|
||||
button.hidden = !isActive();
|
||||
@@ -227,6 +282,30 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate }) {
|
||||
};
|
||||
}
|
||||
|
||||
function createTodayBudgetReplan({ timer, openPlan }) {
|
||||
let active = false;
|
||||
let resume = false;
|
||||
return {
|
||||
open() {
|
||||
if (active) return false;
|
||||
const state = timer.snapshot();
|
||||
if (!state.identity) return false;
|
||||
resume = Boolean(state.running);
|
||||
if (resume && timer.pause() === false) return false;
|
||||
active = true;
|
||||
openPlan?.(state);
|
||||
return true;
|
||||
},
|
||||
restore() {
|
||||
if (!active) return false;
|
||||
active = false;
|
||||
const shouldResume = resume;
|
||||
resume = false;
|
||||
return !shouldResume || timer.resume() !== false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTodayInterruptionPrompt({ timer, sheet, description, getItemLabel, onResolved }) {
|
||||
const render = pending => {
|
||||
if (!pending) {
|
||||
@@ -255,5 +334,6 @@ function createTodayInterruptionPrompt({ timer, sheet, description, getItemLabel
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
createTodayTimer.createView = createTodayTimerView;
|
||||
createTodayTimer.createInterruptionPrompt = createTodayInterruptionPrompt;
|
||||
createTodayTimer.createBudgetReplan = createTodayBudgetReplan;
|
||||
module.exports = createTodayTimer;
|
||||
}
|
||||
@@ -150,12 +150,15 @@ function createTodayWork({ storage, getLogin, limit = 5 }) {
|
||||
}
|
||||
|
||||
function runway(items, currentIndex = 0) {
|
||||
const estimates = planning().estimates;
|
||||
const remaining = (items || []).slice(Math.max(0, currentIndex));
|
||||
const minutes = remaining.map(item => estimates[identity(item)] || null);
|
||||
const plan = planning();
|
||||
const minutes = (items || []).slice(Math.max(0, currentIndex)).map(item => plan.estimates[identity(item)] || null);
|
||||
const total = values => values.some(value => value === null) ? null :
|
||||
values.reduce((sum, value) => sum + value, 0);
|
||||
return {
|
||||
current_minutes: minutes[0] || null,
|
||||
remaining_minutes: minutes.some(value => value === null) ? null : minutes.reduce((total, value) => total + value, 0),
|
||||
remaining_minutes: total(minutes),
|
||||
future_minutes: total(minutes.slice(1)),
|
||||
capacity_minutes: plan.capacity_minutes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user