function createTodayCompletion({ todayWork, todaySync, workSession, refresh, warm, announce, advance = null, completeActivation = () => globalThis.stackchainFirstTaskCompleting?.(), now = Date.now, ttlMs = 10000, onOffer = null, onClear = null, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, undo = [globalThis.document?.getElementById('today-completion-undo'), globalThis.document?.getElementById('undo-today-completion')] }) { const [undoReceipt, undoControl] = undo; let pending = null; let expiryTimer = null; function clear() { if (expiryTimer !== null) clearTimer?.(expiryTimer); expiryTimer = null; pending = null; if (undoReceipt) undoReceipt.hidden = true; onClear?.(); } function completeTodayItem(item, options = {}) { const identity = item && todayWork.identity(item); const snapshot = item && (todayWork.capture?.(item) || (identity ? { id: identity, index: 0, ids: [identity], plan: { capacity_minutes: null, estimates: {} } } : null)); if (!snapshot || !todayWork.remove(item)) { announce(options.failureMessage || 'Could not update Today on this device. Try again.'); return false; } const operations = [ {action:'remove', item_id:identity}, ...(completeActivation() ? [{action:'activate', item_id:'first-task', activation_state:'complete'}] : []), ]; const admitted = todaySync.enqueueBatch ? todaySync.enqueueBatch(operations) : todaySync.enqueue('remove', identity); if (!admitted) { todayWork.restore?.(snapshot); announce(options.admissionFailureMessage || 'Device storage is full. Free space and try again.'); return false; } todaySync.flush(); refresh(); warm(); (advance || (() => workSession.complete()))(); announce(options.successMessage || 'Done for Today. The Gitea item is unchanged.'); pending = { snapshot, expires_at: now() + ttlMs }; if (undoReceipt) undoReceipt.hidden = false; if (undoControl) undoControl.disabled = false; onOffer?.({ identity: snapshot.id, expires_at: pending.expires_at }); expiryTimer = setTimer?.(clear, ttlMs) ?? null; expiryTimer?.unref?.(); return true; } completeTodayItem.undo = () => { if (!pending) return 'missing'; if (now() >= pending.expires_at) { clear(); announce('Undo expired. Add the item back to Today from My Work.'); return 'expired'; } const snapshot = pending.snapshot; const result = todayWork.restore?.(snapshot) || 'unavailable'; if (result !== 'restored') { const messages = { full: 'Today is full. Remove another item before adding this work back.', changed: 'Today changed on this device. Add the item back from My Work.', unavailable: 'Could not restore Today on this device. Add the item back from My Work.', }; clear(); announce(messages[result] || messages.unavailable); return result; } todaySync.enqueue('add', snapshot.id); for (let index = snapshot.ids.length - 1; index > snapshot.index; index -= 1) { todaySync.enqueue('move', snapshot.id, 'up'); } todaySync.flush(); refresh(); warm(); clear(); announce('Restored to Today. Your current work session is unchanged.'); return 'restored'; }; completeTodayItem.clear = clear; undoControl?.addEventListener?.('click', () => { undoControl.disabled = true; if (completeTodayItem.undo() !== 'restored') undoControl.disabled = false; }); return completeTodayItem; } if (typeof module !== 'undefined' && module.exports) module.exports = createTodayCompletion;