function createTodayProgress({ storage, getLogin = () => '', admit, makeId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random().toString(16).slice(2), maxLength = 2000, maxItems = 20 }) { const prefix = 'stackchain.today-progress.v1.'; const login = () => String(getLogin() || '').trim().toLowerCase(); const storageKey = () => login() ? prefix + encodeURIComponent(login()) : ''; const validIdentity = identity => typeof identity === 'string' && identity.length > 0 && identity.length <= 500; const validRecord = record => record && typeof record.body === 'string' && record.body.length > 0 && record.body.length <= maxLength && typeof record.operation_id === 'string' && record.operation_id.length > 0 && record.operation_id.length <= 128; function read() { const key = storageKey(); if (!storage || !key) return {}; try { const saved = JSON.parse(storage.getItem(key) || 'null'); if (!saved || saved.version !== 1 || !saved.drafts || typeof saved.drafts !== 'object' || Array.isArray(saved.drafts)) return {}; const entries = Object.entries(saved.drafts).filter(([identity, record]) => validIdentity(identity) && validRecord(record) ).slice(0, maxItems); return Object.fromEntries(entries); } catch (_error) { return {}; } } function write(drafts) { const key = storageKey(); if (!storage || !key) return false; try { if (Object.keys(drafts).length) storage.setItem(key, JSON.stringify({ version:1, drafts })); else storage.removeItem(key); return true; } catch (_error) { return false; } } function load(identity) { if (!validIdentity(identity)) return ''; return read()[identity]?.body || ''; } function save(identity, value) { if (!validIdentity(identity) || !storageKey()) return false; const body = String(value || '').trim(); if (body.length > maxLength) return false; const drafts = read(); if (!body) { delete drafts[identity]; return write(drafts); } if (!drafts[identity] && Object.keys(drafts).length >= maxItems) return false; const previous = drafts[identity]; drafts[identity] = { body, operation_id: previous?.body === body ? previous.operation_id : String(makeId()).slice(0, 128), }; return write(drafts); } function validTarget(target) { return target && ['issue', 'pull'].includes(target.kind) && validIdentity(target.identity) && typeof target.repository === 'string' && target.repository.length > 0 && target.repository.length <= 200 && Number.isInteger(target.number) && target.number > 0; } async function post(target, value) { if (!validTarget(target)) throw new Error('An active Today issue or pull request is required.'); if (value !== undefined && !save(target.identity, value)) throw new Error('Progress update could not be saved on this device.'); const record = read()[target.identity]; if (!record) throw new Error('Write a progress update before posting.'); if (typeof admit !== 'function') throw new Error('Progress update delivery is unavailable.'); const admission = await admit({ kind:target.kind + '-comment', repository:target.repository, number:target.number, body:record.body, operationId:record.operation_id, }); const drafts = read(); if (drafts[target.identity]?.operation_id === record.operation_id) { delete drafts[target.identity]; write(drafts); } return admission; } return { load, save, discard:identity => save(identity, ''), post }; } function createTodayProgressView({ progress, currentTarget, qs, announce = () => {}, onAdmitted = () => {} }) { const sheet = qs('#today-progress-sheet'); const body = qs('#today-progress-body'); const status = qs('#today-progress-status'); const launcher = qs('[data-mobile-today-update]'); let openedTarget = null; const update = () => { launcher.hidden = !currentTarget(); }; const checkpoint = () => { if (!openedTarget) return false; if (progress.save(openedTarget.identity, body.value)) return true; status.textContent = 'Update must be 2,000 characters or fewer and device storage must be available.'; return false; }; const close = () => { if (sheet.open) sheet.close(); openedTarget = null; }; launcher.addEventListener('click', () => { const target = currentTarget(); if (!target) return; openedTarget = target; qs('#today-progress-target').textContent = target.label + (target.title ? ' · ' + target.title : ''); body.value = progress.load(target.identity); status.textContent = ''; sheet.showModal(); body.focus(); }); qs('#cancel-today-progress').addEventListener('click', () => { if (checkpoint()) close(); }); sheet.addEventListener('cancel', event => { event.preventDefault(); if (checkpoint()) close(); }); qs('#save-today-progress').addEventListener('click', () => { if (!checkpoint()) return; announce('Progress update saved privately to this Today item.'); close(); }); qs('#post-today-progress').addEventListener('click', async () => { const target = currentTarget(); if (!target || target.identity !== openedTarget?.identity) { status.textContent = 'The active Today item changed. Close and open its update again.'; return; } const button = qs('#post-today-progress'); button.disabled = true; status.textContent = 'Saving for delivery…'; try { const admission = await progress.post(target, body.value); onAdmitted(admission); announce(admission.background ? 'Progress update queued for delivery. Today is still on the same item.' : 'Progress update saved for next launch. Today is still on the same item.'); close(); } catch (error) { status.textContent = error.message + ' Your update remains on this item; retry.'; body.focus(); } finally { button.disabled = false; } }); update(); return { update }; } if (typeof module !== 'undefined' && module.exports) { module.exports = createTodayProgress; module.exports.createView = createTodayProgressView; }