feat: launch planned days with morning reminders (Closes #1166)
CI / lint (pull_request) Successful in 3m18s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Failing after 3m39s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-20 05:32:50 +00:00
1 parent 99b60e5f5f
commit d86e84253a
21 files changed
+614 -34

No files matched your search

+30 -3
View File
@@ -476,10 +476,15 @@
refreshMyWorkView();
warmTodayOffline();
},
onRemotePlan: plan => {
onRemotePlan: async plan => {
if (!planningOwnerLogin) return;
latestTodayPlan = plan;
promoteTomorrowIfDue(plan);
const startDayLaunch = window.location.hash === '#/my-work/start-day';
await promoteTomorrowIfDue(plan);
if (startDayLaunch) {
window.history.replaceState({}, '', '#/my-work/today');
openMobileStartDay();
}
todayWork.replacePlanning({
capacity_minutes: plan.capacity_minutes ?? null,
estimates: plan.estimates || {},
@@ -8089,7 +8094,10 @@
if (workSession.active()) workSession.reconcile();
});
let pushController = null;
for (const selector of [qs('#push-deadline-hour'), qs('#device-setup-deadline-hour')]) {
for (const selector of [
qs('#push-deadline-hour'), qs('#device-setup-deadline-hour'),
qs('#push-start-day-hour'), qs('#device-setup-start-day-hour'),
]) {
for (let hour = 0; hour < 24; hour += 1) {
const option = document.createElement('option');
option.value = String(hour);
@@ -8112,6 +8120,13 @@
qs('#device-setup-deadline-days').addEventListener('change', event => {
qs('#push-deadline-days').value = event.target.value;
});
qs('#push-start-day-hour').addEventListener('change', event => {
qs('#device-setup-start-day-hour').value = event.target.value;
if (qs('#push-start-day').checked) pushController?.changeStartDay();
});
qs('#device-setup-start-day-hour').addEventListener('change', event => {
qs('#push-start-day-hour').value = event.target.value;
});
let pushControllerReady = Promise.resolve(null);
if ('serviceWorker' in navigator) {
pushControllerReady = navigator.serviceWorker.register('service-worker.js').then(async () => {
@@ -8124,6 +8139,9 @@
deadlineStatus:qs('#push-deadline-status'),
deadlineHour:qs('#push-deadline-hour'),
deadlineDays:qs('#push-deadline-days'),
startDayControl:qs('#push-start-day'),
startDayStatus:qs('#push-start-day-status'),
startDayHour:qs('#push-start-day-hour'),
deadlineSnooze:qs('#deadline-snooze'),
deadlineSnoozeStatus:qs('#deadline-snooze-status'),
deadlineSnoozeReview:qs('#review-snoozed-deadlines'),
@@ -8142,6 +8160,7 @@
pushController = controller;
qs('#device-setup-deadline-hour').value = qs('#push-deadline-hour').value;
qs('#device-setup-deadline-days').value = qs('#push-deadline-days').value;
qs('#device-setup-start-day-hour').value = qs('#push-start-day-hour').value;
return controller;
}).catch(error => {
qs('#push-updates').disabled = true;
@@ -8152,6 +8171,14 @@
qs('#push-updates').disabled = true;
qs('#push-update-status').textContent = 'This browser does not support update notifications.';
}
qs('#device-setup-start-day').addEventListener('click', async () => {
const controller = await pushControllerReady;
if (!controller) return;
qs('#push-start-day-hour').value = qs('#device-setup-start-day-hour').value;
qs('#push-start-day').checked = true;
await controller.changeStartDay();
qs('#device-setup-start-day-status').textContent = qs('#push-start-day-status').textContent;
});
let deferredInstallPrompt = null;
window.addEventListener('beforeinstallprompt', event => {
event.preventDefault();
+11
View File
@@ -143,6 +143,14 @@
<button class="device-setup-action" id="device-setup-deadline" type="button">Enable</button>
</div>
</li>
<li class="device-setup-step">
<div><strong>Start my planned day</strong><p class="small" id="device-setup-start-day-status" role="status">Get one private reminder when Tomorrow is ready.</p></div>
<div class="device-setup-deadline-controls">
<label for="device-setup-start-day-hour">Local hour</label>
<select id="device-setup-start-day-hour" aria-label="Start-day reminder local hour"></select>
<button class="device-setup-action" id="device-setup-start-day" type="button">Enable</button>
</div>
</li>
</ol>
<section class="device-storage" aria-labelledby="device-storage-heading">
<div>
@@ -209,6 +217,9 @@
<label for="push-deadline-hour">Reminder hour <select id="push-deadline-hour" aria-label="Deadline reminder local hour"></select></label>
<label for="push-deadline-days">Warn me <select id="push-deadline-days" aria-label="Deadline reminder horizon"><option value="0">Due today</option><option value="2" selected>Next 2 days</option><option value="7">Next 7 days</option></select></label>
<span class="small" id="push-deadline-status" role="status" aria-live="polite"></span>
<label class="push-update-control" for="push-start-day"><input id="push-start-day" type="checkbox" /> Remind me when my planned day is ready</label>
<label for="push-start-day-hour">Start-day hour <select id="push-start-day-hour" aria-label="Start-day reminder local hour"></select></label>
<span class="small" id="push-start-day-status" role="status" aria-live="polite"></span>
<label class="push-update-control" for="today-lock-screen"><input id="today-lock-screen" type="checkbox" /> Show active Today controls on my lock screen</label>
<span class="small" id="today-lock-screen-status" role="status" aria-live="polite"></span>
<button id="clear-offline-work" type="button">Clear offline work data</button>
+54 -2
View File
@@ -3,6 +3,7 @@
else root.createPushNotifications = factory;
})(typeof self !== 'undefined' ? self : this, function createPushNotifications({
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
startDayControl, startDayStatus, startDayHour,
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview, onReviewDeadlines,
notification, serviceWorker, fetchJson,
}) {
@@ -127,8 +128,10 @@
control.checked = false;
if (testControl) testControl.hidden = true;
if (deadlineControl) deadlineControl.checked = false;
if (startDayControl) startDayControl.checked = false;
configuration.subscribed = false;
configuration.deadline_enabled = false;
configuration.start_day_enabled = false;
pendingIntent = null;
status.textContent = 'New update notifications are off for this device.';
if (deadlineStatus) deadlineStatus.textContent = 'Deadline reminders are off for this device.';
@@ -225,13 +228,51 @@
}
}
async function changeStartDay() {
startDayControl.disabled = true;
if (startDayHour) startDayHour.disabled = true;
try {
const registration = await serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (startDayControl.checked) pendingIntent = 'start-day';
if (startDayControl.checked && !subscription) subscription = await ensureSubscription();
if (startDayControl.checked && !subscription) {
startDayControl.checked = false;
startDayStatus.textContent = status.textContent;
return false;
}
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const reminderHour = Number(startDayHour?.value ?? configuration?.start_day_reminder_hour ?? 9);
await fetchJson('api/v1/push-subscription/start-day', {
method:'PUT',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({enabled:startDayControl.checked, timezone, reminder_hour:reminderHour}),
});
configuration.start_day_enabled = startDayControl.checked;
configuration.start_day_timezone = timezone;
configuration.start_day_reminder_hour = reminderHour;
pendingIntent = null;
startDayStatus.textContent = startDayControl.checked
? `Start-day reminder enabled for ${formattedHour(reminderHour)} local time.`
: 'Start-day reminders are off for this device.';
return true;
} catch (_error) {
startDayControl.checked = !startDayControl.checked;
startDayStatus.textContent = 'Could not change start-day reminders. Check your connection and try again.';
return false;
} finally {
startDayControl.disabled = false;
if (startDayHour) startDayHour.disabled = false;
}
}
async function enableDeadline() {
deadlineControl.checked = true;
return changeDeadline();
}
async function recoverPermission(intent = null) {
if (!pendingIntent && (intent === 'updates' || intent === 'deadline')) pendingIntent = intent;
if (!pendingIntent && ['updates', 'deadline', 'start-day'].includes(intent)) pendingIntent = intent;
if (!pendingIntent || notification.permission !== 'granted') return false;
if (recoveryPromise) return recoveryPromise;
recoveryPromise = (async () => {
@@ -239,6 +280,10 @@
deadlineControl.checked = true;
return changeDeadline();
}
if (pendingIntent === 'start-day') {
startDayControl.checked = true;
return changeStartDay();
}
return Boolean(await enable());
})();
try {
@@ -253,24 +298,31 @@
control.addEventListener('change', change);
testControl?.addEventListener('click', testDelivery);
deadlineControl?.addEventListener('change', changeDeadline);
startDayControl?.addEventListener('change', changeStartDay);
deadlineSnoozeReview?.addEventListener('click', reviewSnoozedDeadlines);
configuration = await fetchJson('api/v1/push-subscription');
if (!configuration.available) {
control.disabled = true;
if (deadlineControl) deadlineControl.disabled = true;
if (startDayControl) startDayControl.disabled = true;
status.textContent = 'New update notifications are not available on this server.';
return;
}
control.checked = Boolean(configuration.subscribed);
if (deadlineControl) deadlineControl.checked = Boolean(configuration.deadline_enabled);
if (startDayControl) startDayControl.checked = Boolean(configuration.start_day_enabled);
if (deadlineHour) deadlineHour.value = String(configuration.reminder_hour ?? 9);
if (deadlineDays) deadlineDays.value = String(configuration.reminder_days ?? 2);
if (startDayHour) startDayHour.value = String(configuration.start_day_reminder_hour ?? 9);
renderDeliveryHealth();
if (deadlineStatus) deadlineStatus.textContent = configuration.deadline_enabled
? enabledDeadlineText(configuration.reminder_hour, configuration.reminder_days)
: 'Deadline reminders are off for this device.';
if (startDayStatus) startDayStatus.textContent = configuration.start_day_enabled
? `Start-day reminder enabled for ${formattedHour(configuration.start_day_reminder_hour)} local time.`
: 'Start-day reminders are off for this device.';
renderDeadlineSnooze();
}
return {init, change, changeDeadline, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
return {init, change, changeDeadline, changeStartDay, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
});
+15 -1
View File
@@ -1,7 +1,7 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/private-data-registry.js');
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v124';
const CACHE = 'stackchain-dashboard-shell-v125';
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;
@@ -500,6 +500,20 @@ self.addEventListener('push', event => {
const notificationId = Number(payload.notification_id);
const updateCount = Number(payload.update_count);
const deadlineCount = Number(payload.deadline_count);
const planDate = String(payload.plan_date || '');
if (
route === '#/my-work/start-day'
&& /^\d{4}-\d{2}-\d{2}$/.test(planDate)
&& tag === 'stackchain-start-day-' + planDate
) {
event.waitUntil(self.registration.showNotification('Your planned day is ready', {
body: 'Open Stackchain to prepare Today.',
tag,
actions: [{ action: 'prepare-today', title: 'Prepare Today' }],
data: {route},
}));
return;
}
if (
route === '#/my-work/agenda'
&& protectRoute === '#/my-work/agenda/protect-today'