diff --git a/frontend/agenda-replan.js b/frontend/agenda-replan.js
new file mode 100644
index 0000000..250bd78
--- /dev/null
+++ b/frontend/agenda-replan.js
@@ -0,0 +1,79 @@
+function createAgendaReplan({ now = () => new Date(), update }) {
+ let items = [];
+ let index = 0;
+ let pending = false;
+ let active = false;
+
+ const dayKey = date => [
+ date.getFullYear(),
+ String(date.getMonth() + 1).padStart(2, '0'),
+ String(date.getDate()).padStart(2, '0'),
+ ].join('-');
+ const current = () => active ? items[index] || null : null;
+ const snapshot = () => ({
+ active,
+ index,
+ total: items.length,
+ current: current()?.key || null,
+ pending,
+ });
+ const advance = () => {
+ index += 1;
+ if (index >= items.length) active = false;
+ return { ok:true, done:!active };
+ };
+ const change = async dueDate => {
+ const item = current();
+ if (!item) return { ok:false, error:'No overdue deadline selected.' };
+ if (pending) return { ok:false, error:'Deadline update already in progress.' };
+ pending = true;
+ try {
+ await update(item, dueDate);
+ return advance();
+ } catch (error) {
+ return { ok:false, error:error?.message || 'Deadline could not be updated.' };
+ } finally {
+ pending = false;
+ }
+ };
+
+ return {
+ start(overdue) {
+ items = (overdue || []).slice().sort((left, right) =>
+ new Date(left.due_date).getTime() - new Date(right.due_date).getTime() ||
+ String(left.repository || '').localeCompare(String(right.repository || '')) ||
+ Number(left.number || 0) - Number(right.number || 0)
+ );
+ index = 0;
+ pending = false;
+ active = items.length > 0;
+ return snapshot();
+ },
+ snapshot,
+ current,
+ keep() {
+ if (pending) return Promise.resolve({ ok:false, error:'Deadline update already in progress.' });
+ if (!current()) return Promise.resolve({ ok:false, error:'No overdue deadline selected.' });
+ return Promise.resolve(advance());
+ },
+ tomorrow() {
+ const date = new Date(now());
+ date.setDate(date.getDate() + 1);
+ return change(dayKey(date) + 'T23:59:59Z');
+ },
+ choose(value) {
+ const chosen = String(value || '');
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(chosen) || chosen <= dayKey(now())) {
+ return Promise.resolve({ ok:false, error:'Choose a future date.' });
+ }
+ return change(chosen + 'T23:59:59Z');
+ },
+ cancel() {
+ active = false;
+ pending = false;
+ return snapshot();
+ },
+ };
+}
+
+if (typeof module !== 'undefined' && module.exports) module.exports = createAgendaReplan;
diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index f103e24..250cb0c 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -243,6 +243,14 @@ textarea { resize: vertical; min-height: 120px; }
.draft-filing-session-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.draft-filing-session-actions button { min-height:44px; width:100%; }
.my-work-card { min-height: 44px; display:grid; gap:8px; padding:12px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; color:var(--text); }
+.agenda-replan { margin:10px 0; padding:12px; border:1px solid #7c4a1d; border-radius:12px; background:#24170d; }
+.agenda-replan-launch { display:flex; align-items:center; justify-content:space-between; gap:12px; }
+.agenda-replan-launch p { margin:4px 0 0; }
+.agenda-replan-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:10px; }
+.agenda-replan-actions button { min-height:44px; width:100%; }
+.agenda-replan-actions label { grid-column:1/-1; }
+.agenda-replan-actions input { box-sizing:border-box; min-height:44px; width:100%; max-width:100%; }
+@media(max-width:360px) { .agenda-replan-launch { align-items:stretch; flex-direction:column; } .agenda-replan-actions { grid-template-columns:1fr; } .agenda-replan-actions label { grid-column:auto; } }
.my-work-card-main { display:block; width:100%; color:var(--text); text-align:left; font:inherit; background:transparent; border:0; padding:0; }
.my-work-card-main.review-trigger { width:100%; text-align:left; font:inherit; }
.my-work-card:hover { border-color:var(--accent); }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 3bf330b..1cf8f6c 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -149,6 +149,7 @@
let notificationPagination = { page: 1, total: 0, has_more: false };
let workPagination = {};
let agendaChecking = false;
+ let agendaReplan = null;
let hasContextSnapshot = false;
let selectedReview = null;
let reviewTrigger = null;
@@ -329,6 +330,62 @@
let reviewController = null;
let wrapPreference = null;
const issueController = createIssueSheet({ fetchJson: fetchReviewJson, storage: localStorage });
+ function overdueAgendaItems() {
+ return agendaMyWork(activeMyWork).filter(item => item.agenda_group === 'Overdue');
+ }
+ function renderAgendaReplan() {
+ const state = agendaReplan.snapshot();
+ qs('#agenda-replan-controls').hidden = !state.active;
+ if (!state.active) return;
+ qs('#agenda-replan-progress').textContent =
+ 'Overdue deadline ' + (state.index + 1) + ' of ' + state.total + ' ยท ' + (agendaReplan.current()?.key || '');
+ }
+ function openAgendaReplanCurrent() {
+ const item = agendaReplan.current();
+ if (!item) {
+ closeIssueSheet(false);
+ renderAgendaReplan();
+ qs('#my-work-action-status').textContent = 'Overdue sweep complete. Agenda updated.';
+ return;
+ }
+ renderAgendaReplan();
+ openIssueSheet(item, qs('#start-agenda-replan'));
+ }
+ agendaReplan = createAgendaReplan({
+ update: async (item, dueDate) => {
+ const confirmed = await issueController.updateDueDate(item, dueDate);
+ lastContextSnapshot = buildMyWork.replaceIssueDueDate(
+ lastContextSnapshot, item.repository, item.number, confirmed.due_date
+ );
+ paintMyWork(lastContextSnapshot);
+ return confirmed;
+ },
+ });
+ qs('#start-agenda-replan').addEventListener('click', () => {
+ const overdue = overdueAgendaItems();
+ agendaReplan.start(overdue);
+ openAgendaReplanCurrent();
+ });
+ async function runAgendaReplan(action) {
+ const result = await action();
+ if (!result.ok) {
+ qs('#agenda-replan-progress').textContent = result.error + ' Current deadline retained; retry.';
+ return;
+ }
+ openAgendaReplanCurrent();
+ }
+ qs('#agenda-replan-keep').addEventListener('click', () => runAgendaReplan(() => agendaReplan.keep()));
+ qs('#agenda-replan-tomorrow').addEventListener('click', () => runAgendaReplan(() => agendaReplan.tomorrow()));
+ qs('#agenda-replan-choose').addEventListener('click', () =>
+ runAgendaReplan(() => agendaReplan.choose(qs('#agenda-replan-date').value))
+ );
+ qs('#agenda-replan-cancel').addEventListener('click', () => {
+ agendaReplan.cancel();
+ closeIssueSheet(false);
+ renderAgendaReplan();
+ window.location.hash = '#/my-work/agenda';
+ qs('#my-work-action-status').textContent = 'Overdue sweep cancelled. No remaining deadline was changed.';
+ });
const issueAttachmentController = issueAttachment.mount({
input: qs('#issue-attachment'),
preview: qs('#issue-attachment-preview'),
@@ -2449,6 +2506,13 @@
filterMyWork(laterMyWork, 'all', selectedWorkMilestone) : selectedWorkFilter === 'agenda' ?
agendaMyWork(activeMyWork) :
filterMyWork(activeMyWork, selectedWorkFilter, selectedWorkMilestone);
+ const overdue = queueItems.filter(item => item.agenda_group === 'Overdue');
+ const replanPanel = qs('#agenda-replan');
+ replanPanel.hidden = selectedWorkFilter !== 'agenda' || overdue.length === 0;
+ if (!agendaReplan?.snapshot().active) {
+ qs('#start-agenda-replan').textContent = 'Replan overdue (' + overdue.length + ')';
+ qs('#agenda-replan-controls').hidden = true;
+ }
const incomplete = activeWorkStreams().some(stream => workPagination[stream]?.has_more);
const visible = findQueueItems(queueItems, queueFindQuery);
const emptyWorkStart = qs('#empty-work-start');
diff --git a/frontend/index.html b/frontend/index.html
index fe8bab5..f9b0d1b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -193,6 +193,23 @@
+ Make the Agenda credible without leaving the queue.